fix(upload-binary): prevent SSRF via x-proxy-target-url allowlist - #302
Open
sebastionoss wants to merge 1 commit into
Open
fix(upload-binary): prevent SSRF via x-proxy-target-url allowlist#302sebastionoss wants to merge 1 commit into
sebastionoss wants to merge 1 commit into
Conversation
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
The
/api/upload-binaryand/api/v1/upload-binaryroutes act as a server-side proxy for S3 uploads. The destination is taken directly from thex-proxy-target-urlfield on the incoming multipart form and passed tofetch()without any validation. That makes both endpoints a Server-Side Request Forgery (SSRF) sink (CWE-918): an attacker who can reach the Next.js server can coerce it into issuing arbitrary outbound requests — including to internal services or cloud metadata endpoints such ashttp://169.254.169.254/latest/meta-data/.The routes have no auth gate — there is no
middleware.tsat the project root and no per-route auth check — so any client that can reach the app can trigger the proxy.app/api/upload-binary/route.js,app/api/v1/upload-binary/route.jsfetch(targetUrl, { method: 'POST', body: s3FormData })wheretargetUrl = formData.get('x-proxy-target-url')Fix
Both routes now validate
x-proxy-target-urlagainst a small allowlist before callingfetch():URLconstructor (so userinfo tricks likehttps://s3.amazonaws.com@169.254.169.254/don't smuggle a different host past the check).https:(blocksfile:,gopher:,http:to plaintext internal services)..amazonaws.com,.s3.amazonaws.com,.cloudfront.net,.muapi.ai. The check is suffix-anchored on.<suffix>so lookalikes likeamazonaws.com.evil.comdon't match.169.254.169.254,127.0.0.1, decimal/hex/octal encodings, and[::1]are all rejected.Rejected requests return
400 Invalid proxy target URL. Legitimate presigned URLs returned by MuAPI (which point at AWS S3 / CloudFront) continue to work unchanged.Proof of concept
Before the fix, against a local dev server:
After the fix, both requests return
{"error":"Invalid proxy target URL"}with HTTP 400. Presigned URLs of the formhttps://<bucket>.s3.amazonaws.com/...continue to be proxied normally.What was tested
The allowlist logic was exercised against the following cases; all bypass attempts are rejected and all legitimate targets are accepted:
http://169.254.169.254/latest/meta-data/,http://127.0.0.1:6379/,http://[::1]/,http://2130706433/,http://0x7f000001/,file:///etc/passwd,gopher://127.0.0.1:6379/_INFO,https://amazonaws.com.evil.com/,https://evil.com/amazonaws.com,https://s3.amazonaws.com@169.254.169.254/,not-a-url, empty string.https://my-bucket.s3.amazonaws.com/key,https://d123.cloudfront.net/asset,https://api.muapi.ai/upload.Adversarial review
Before submitting we tried to disprove the finding. The routes are part of a self-hostable app, so we asked whether an attacker realistically reaches them and whether existing controls block exploitation. They don't: there is no auth middleware, no host/CORS restriction on the route, and the
fetch()call trusts the raw form value. Anyone who can reach the Next.js server on the network — including LAN peers on a developer's machine and anyone hitting a publicly-exposed deployment — can drive the proxy. AWS IMDSv2 mitigates the metadata angle on modern EC2, but IMDSv1 is still common on self-hosted setups and internal-service pivots (Redis, Docker socket over TCP, admin panels on loopback) don't depend on cloud metadata at all. The fix is a positive allowlist rather than a denylist, which avoids the usual round of bypasses (encoded IPs, IPv6, DNS rebinding on the hostname string, userinfo@confusion).Discovered by the Sebastion AI GitHub App.