Repository navigation
server side request forgery filter - #4
Conversation
| if (!isSafeHost(u.getHost())) { //check host of the URL object we actually connect to | ||
| throw new IOException("blocked request to non-public host " + u.getHost()); | ||
| } | ||
| HttpURLConnection c = (HttpURLConnection)u.openConnection(); |
There was a problem hiding this comment.
Note
Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.
Devin Review found 5 potential issues.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| if (!isSafeHost(u.getHost())) { //check host of the URL object we actually connect to | ||
| throw new IOException("blocked request to non-public host " + u.getHost()); | ||
| } | ||
| HttpURLConnection c = (HttpURLConnection)u.openConnection(); |
There was a problem hiding this comment.
🟥 DNS rebinding reaches private services
For an attacker-controlled hostname, isSafeHost can approve a public DNS answer before the connection resolves a private one. The request then reaches an internal service despite passing the outbound filter.
Was this helpful? React with 👍 or 👎 to provide feedback.
| private static boolean isSafeIPv4(byte[] b, int off) { | ||
| int a0 = b[off] & 0xff, a1 = b[off + 1] & 0xff, a2 = b[off + 2] & 0xff; | ||
| return !(a0 == 0 || //0.0.0.0/8 "this" network | ||
| a0 == 10 || //10.0.0.0/8 private | ||
| (a0 == 100 && a1 >= 64 && a1 <= 127) || //100.64.0.0/10 carrier grade NAT, includes Alibaba metadata | ||
| a0 == 127 || //127.0.0.0/8 loopback | ||
| (a0 == 169 && a1 == 254) || //169.254.0.0/16 link local, includes AWS/GCP/Azure metadata | ||
| (a0 == 172 && a1 >= 16 && a1 <= 31) || //172.16.0.0/12 private | ||
| (a0 == 192 && a1 == 0 && a2 == 0) || //192.0.0.0/24 IETF protocol assignments, includes OCI metadata | ||
| (a0 == 192 && a1 == 168) || //192.168.0.0/16 private | ||
| (a0 == 198 && (a1 == 18 || a1 == 19)) || //198.18.0.0/15 benchmarking | ||
| a0 >= 224); //224.0.0.0/4 multicast, 240.0.0.0/4 reserved, 255.255.255.255 broadcast |
There was a problem hiding this comment.
| if (!isSafeHost(u.getHost())) { //check host of the URL object we actually connect to | ||
| throw new IOException("blocked request to non-public host " + u.getHost()); | ||
| } | ||
| HttpURLConnection c = (HttpURLConnection)u.openConnection(); |
S3 URLs are fetched by the S3 client, never HTTPHelper, so the check did not protect them. With a PrivateLink interface endpoint and private DNS, regional S3 hostnames resolve to VPC addresses, so the check rejected valid images. RDR_URL_WHITELIST_PATTERNS still applies to S3 URLs. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
jl-0
left a comment
There was a problem hiding this comment.
Looks good, added a check in DziTask.java for S3 urls since private VPCs with S3 links is a real concern
…ice into feature/ssrf-filter # Conflicts: # src/main/java/jpl/mipl/mars/tile_service/DziTask.java
| //[BUCKET.]s3[.-][REGION.]amazonaws.com, also matches e.g. dualstack and website endpoints | ||
| //host labels must be delimited, e.g. evil-s3amazonaws.com and s3.evilamazonaws.com do not match | ||
| private static final Pattern S3_HOST_PATTERN = | ||
| Pattern.compile("([a-z0-9.-]+\\.)?s3[.-]([a-z0-9.-]+\\.)?amazonaws\\.com"); |
There was a problem hiding this comment.
| if (url.startsWith("/") || Paths.get(url).isAbsolute()) { | ||
| return false; |
There was a problem hiding this comment.
🟥 Relative paths escape the input directory
With allowDotDotInput enabled, filterUrl accepts a relative ../ path that escapes fs_input_dir. DziParams then prefixes that path without resolving it, exposing files outside the configured input directory.
Was this helpful? React with 👍 or 👎 to provide feedback.
flynnplatt
left a comment
There was a problem hiding this comment.
This all seems fine to me. Devin/Co-Pilot have some thoughts, some of which seem not totally useless so I wouldn't be opposed to you adding some of like the S3 url protections here but I'm also happy to merge as-is
| String path = Utils.stripFileScheme(url); | ||
| try { | ||
| if (path.startsWith("/") || Paths.get(path).isAbsolute()) { |
There was a problem hiding this comment.
🟡 File aliases reject root images
For a root-level ICM-test.IMG, filterUrl accepts the bare path but rejects its file:// alias. The default ICM pattern matches the alias's slash, so equivalent image requests return different results.
Learn more
The image filter applies its default ICM exclusion to the URL before removing the file:// prefix. That prefix supplies a slash immediately before a root-level filename, changing whether the pattern matches. DziParams strips the prefix before resolving the input, so both requests target the same file. The ICM check still evaluates the unstripped URL.
Example: With fs_input_dir=test/, ICM-test.IMG passes the default ICM exclusion, but file://ICM-test.IMG matches .*/ICM-[^/]* and returns 400.
Recommended fix: Keep the original URL for whitelist validation, then use the stripped local path consistently for subsequent filename and directory filtering. Add a test comparing the two forms for a root-level ICM image.
Was this helpful? React with 👍 or 👎 to provide feedback.
|




harden URL handling in server side requests