-
Notifications
You must be signed in to change notification settings - Fork 79
fix(security): prevent SSRF and credential leaks in URL fetching #1061
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vinokurig
wants to merge
7
commits into
main
Choose a base branch
from
CRW-11956
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a55daf3
fix(security): prevent SSRF and credential leaks in URL fetching (CRW…
vinokurig d9a5a74
fix(security): check the destination of SCM requests built from user …
vinokurig cdc1672
fix(security): reject 0.0.0.0/8 and the RFC 5737 ranges (CRW-11956)
vinokurig d60c367
Merge branch 'main' of github.com:eclipse-che/che-server into CRW-11956
vinokurig 49bf508
Potential fix for pull request finding 'CodeQL / Server-side request …
vinokurig 868b143
Merge branch 'CRW-11956' of github.com:eclipse-che/che-server into CR…
vinokurig a2c0f3e
chore(security): document why the devfile URL fetch is user supplied …
vinokurig File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
137 changes: 137 additions & 0 deletions
137
.../che-core-commons-lang/src/main/java/org/eclipse/che/commons/lang/UrlTargetValidator.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| /* | ||
| * Copyright (c) 2012-2026 Red Hat, Inc. | ||
| * This program and the accompanying materials are made | ||
| * available under the terms of the Eclipse Public License 2.0 | ||
| * which is available at https://www.eclipse.org/legal/epl-2.0/ | ||
| * | ||
| * SPDX-License-Identifier: EPL-2.0 | ||
| * | ||
| * Contributors: | ||
| * Red Hat, Inc. - initial API and implementation | ||
| */ | ||
| package org.eclipse.che.commons.lang; | ||
|
|
||
| import static com.google.common.base.Strings.isNullOrEmpty; | ||
|
|
||
| import java.io.IOException; | ||
| import java.net.InetAddress; | ||
| import java.net.URI; | ||
| import java.net.URISyntaxException; | ||
| import java.net.UnknownHostException; | ||
|
|
||
| /** | ||
| * Checks that a URL derived from user input points at a target the server is allowed to reach. | ||
| * | ||
| * <p>Several endpoints take a URL from the user and make the server fetch it. Without a check on | ||
| * the destination, such a URL can be aimed at a service that is only reachable from inside the | ||
| * cluster - the cloud metadata endpoint, the Kubernetes API, a neighbouring pod - turning the | ||
| * server into a proxy for the caller (SSRF). | ||
| */ | ||
| public final class UrlTargetValidator { | ||
|
|
||
| private UrlTargetValidator() {} | ||
|
|
||
| /** | ||
| * Throws if the given URL may not be requested by the server, either because of its scheme or | ||
| * because its host resolves to an address that is not publicly routable. | ||
| * | ||
| * @param url the URL about to be requested | ||
| * @throws IOException if the URL is malformed or its target is not allowed | ||
| */ | ||
| public static void validate(String url) throws IOException { | ||
| // the scheme is read off the raw string: an opaque URL such as jar:file:/x!/y is rejected here | ||
| // rather than reported as a parsing failure | ||
| int schemeEnd = url == null ? -1 : url.indexOf(':'); | ||
| String scheme = schemeEnd > 0 ? url.substring(0, schemeEnd) : null; | ||
| if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) { | ||
| throw new IOException( | ||
| "Only http and https URLs are allowed, got: " + scheme + " in URL " + url); | ||
| } | ||
|
|
||
| final URI uri; | ||
| try { | ||
| uri = new URI(url); | ||
| } catch (URISyntaxException e) { | ||
| throw new IOException("Invalid URL " + url, e); | ||
| } | ||
|
|
||
| String host = uri.getHost(); | ||
| if (isNullOrEmpty(host)) { | ||
| throw new IOException("URL host is missing in " + url); | ||
| } | ||
|
|
||
| final InetAddress[] addresses; | ||
| try { | ||
| // all records are checked, so that a host publishing both a public and an internal address | ||
| // cannot pass the check and then be connected to on the internal one | ||
| addresses = InetAddress.getAllByName(host); | ||
| } catch (UnknownHostException e) { | ||
| throw new IOException("Unable to resolve URL host " + host + " in " + url, e); | ||
| } | ||
|
|
||
| for (InetAddress address : addresses) { | ||
| if (!isPubliclyRoutable(address)) { | ||
| throw new IOException("URL host is not allowed: " + host); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Same check as {@link #validate(String)}, in a form usable where a URL is probed on a best | ||
| * effort basis and a disallowed target simply means "not a match". | ||
| * | ||
| * @param url the URL about to be requested | ||
| * @return true if the server may request the URL | ||
| */ | ||
| public static boolean isAllowed(String url) { | ||
| try { | ||
| validate(url); | ||
| return true; | ||
| } catch (IOException e) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Tells whether an address belongs to the public internet, as opposed to the ranges reserved for | ||
| * private networks, the host itself, or protocol machinery. | ||
| */ | ||
| private static boolean isPubliclyRoutable(InetAddress address) { | ||
| if (address.isAnyLocalAddress() | ||
| || address.isLoopbackAddress() | ||
| || address.isLinkLocalAddress() | ||
| // IPv4 private ranges and the deprecated IPv6 site-local fec0::/10 | ||
| || address.isSiteLocalAddress() | ||
| || address.isMulticastAddress()) { | ||
| return false; | ||
| } | ||
|
|
||
| byte[] bytes = address.getAddress(); | ||
| if (bytes.length == 4) { | ||
| int first = bytes[0] & 0xFF; | ||
| int second = bytes[1] & 0xFF; | ||
| // 100.64.0.0/10 shared address space (RFC 6598), used by several CNI plugins | ||
| if (first == 100 && second >= 64 && second <= 127) { | ||
| return false; | ||
| } | ||
| // 192.0.0.0/24 IETF protocol assignments (RFC 6890) | ||
| if (first == 192 && second == 0 && (bytes[2] & 0xFF) == 0) { | ||
| return false; | ||
| } | ||
| // 198.18.0.0/15 benchmarking (RFC 2544) | ||
| if (first == 198 && (second == 18 || second == 19)) { | ||
| return false; | ||
| } | ||
| // 240.0.0.0/4 reserved, including the 255.255.255.255 broadcast address | ||
| if (first >= 240) { | ||
| return false; | ||
| } | ||
| } else if (bytes.length == 16) { | ||
| // fc00::/7 unique local addresses (RFC 4193), which isSiteLocalAddress does not cover | ||
| if ((bytes[0] & 0xFE) == 0xFC) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| } | ||
77 changes: 77 additions & 0 deletions
77
...-core-commons-lang/src/test/java/org/eclipse/che/commons/lang/UrlTargetValidatorTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| /* | ||
| * Copyright (c) 2012-2026 Red Hat, Inc. | ||
| * This program and the accompanying materials are made | ||
| * available under the terms of the Eclipse Public License 2.0 | ||
| * which is available at https://www.eclipse.org/legal/epl-2.0/ | ||
| * | ||
| * SPDX-License-Identifier: EPL-2.0 | ||
| * | ||
| * Contributors: | ||
| * Red Hat, Inc. - initial API and implementation | ||
| */ | ||
| package org.eclipse.che.commons.lang; | ||
|
|
||
| import static org.testng.Assert.assertFalse; | ||
| import static org.testng.Assert.assertTrue; | ||
|
|
||
| import org.testng.annotations.DataProvider; | ||
| import org.testng.annotations.Test; | ||
|
|
||
| /** Tests of {@link UrlTargetValidator}. */ | ||
| public class UrlTargetValidatorTest { | ||
|
|
||
| @DataProvider | ||
| public Object[][] disallowedUrls() { | ||
| return new Object[][] { | ||
| // schemes that are not a web request at all | ||
| {"file:///etc/passwd"}, | ||
| {"ftp://example.com/file"}, | ||
| {"jar:file:///tmp/evil.jar!/payload"}, | ||
| {"gopher://example.com/"}, | ||
| {"no-scheme-at-all"}, | ||
| // the host itself | ||
| {"http://127.0.0.1/"}, | ||
| {"http://[::1]/"}, | ||
| {"http://0/"}, | ||
| {"http://[::ffff:127.0.0.1]/"}, | ||
| // cloud metadata, reachable on the link-local range | ||
| {"http://169.254.169.254/latest/meta-data/"}, | ||
| {"http://[fe80::1]/"}, | ||
| {"http://[0:0:0:0:0:ffff:a9fe:a9fe]/"}, | ||
| // IPv4 private ranges | ||
| {"http://10.0.0.1/"}, | ||
| {"http://172.16.0.1/"}, | ||
| {"http://192.168.1.1/"}, | ||
| // IPv6 unique local addresses, which InetAddress#isSiteLocalAddress does not cover | ||
| {"http://[fc00::1]/"}, | ||
| {"http://[fd12:3456:789a::1]/"}, | ||
| // ranges that are not the public internet either | ||
| {"http://100.64.1.1/"}, | ||
| {"http://192.0.0.1/"}, | ||
| {"http://198.18.0.1/"}, | ||
| {"http://240.0.0.1/"}, | ||
| {"http://255.255.255.255/"}, | ||
| // no host to check | ||
| {"http:///path"}, | ||
| }; | ||
| } | ||
|
|
||
| @Test(dataProvider = "disallowedUrls") | ||
| public void shouldRejectUrl(String url) { | ||
| assertFalse(UrlTargetValidator.isAllowed(url), url + " should not be reachable"); | ||
| } | ||
|
|
||
| @DataProvider | ||
| public Object[][] allowedUrls() { | ||
| return new Object[][] { | ||
| {"https://93.184.216.34/devfile.yaml"}, | ||
| {"http://8.8.8.8/"}, | ||
| {"https://[2001:4860:4860::8888]/"}, | ||
| }; | ||
| } | ||
|
|
||
| @Test(dataProvider = "allowedUrls") | ||
| public void shouldAllowUrl(String url) { | ||
| assertTrue(UrlTargetValidator.isAllowed(url), url + " should be reachable"); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.