fix(security): validate and IP-pin proxy target on /api/upload-binary (SSRF)#184
Open
aaronjmars wants to merge 1 commit into
Open
fix(security): validate and IP-pin proxy target on /api/upload-binary (SSRF)#184aaronjmars wants to merge 1 commit into
aaronjmars wants to merge 1 commit into
Conversation
… (SSRF)
The two /api/upload-binary route handlers accept any URL in the
x-proxy-target-url form field and POST FormData to it server-side. The
intent is to forward to S3 presigned URLs returned by upstream MuAPI,
but with no validation the endpoint is a generic SSRF gadget — an
attacker can point it at cloud metadata (169.254.169.254), internal
admin APIs, RFC1918 hosts, or any URL the Next.js server can reach.
This change extracts a shared validator in app/lib/proxyTargetSafety.mjs
that both routes call before fetch:
- Requires https:// (blocks http://, file://, ftp://, ...).
- Rejects URLs with embedded credentials.
- Rejects literal IPs in private / loopback / link-local /
cloud-metadata / CGNAT / multicast / reserved / IPv6 ULA / IPv6
link-local ranges. Covers IPv4-mapped IPv6 in both dotted and
hex-normalized forms (URL parser canonicalizes
[::ffff:169.254.169.254] to [::ffff:a9fe:a9fe]), IPv4-compatible
IPv6, and 2002::/16 6to4 wrapping a private IPv4.
- DNS-resolves the hostname and rejects the request if any returned
address is in those ranges.
- Pins the connection to the validated IP via an undici Agent so a
racing DNS flip between validation and fetch cannot redirect the
request to a private IP (canonical DNS-rebinding defense).
- Optional operator allowlist via PROXY_TARGET_ALLOWED_HOSTS for
deployments that want to scope this endpoint to a known storage
provider list.
Adds undici as an explicit dependency for the IP-pinned Agent.
Detected by Aeon + manual review of app/api/*/route.js proxy handlers.
Severity: HIGH
CWE-918 (SSRF) + CWE-350/346 (DNS rebinding hardening)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Author
|
Friendly bump — validates and IP-pins the proxy target on |
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
/api/upload-binaryand/api/v1/upload-binaryare server-side proxies that accept any URL in thex-proxy-target-urlform field andPOSTFormData to it. The intent is to forward to S3 presigned URLs the upstream MuAPI service returns, but with no validation those endpoints are generic SSRF gadgets — an attacker can point them at cloud metadata (http://169.254.169.254/...), internal admin APIs, RFC1918 hosts, or any URL the Next.js server can reach. The server returns the upstream response body to the caller on non-2xx, which leaks internal error pages back to the attacker too.This PR adds a shared validator (
app/lib/proxyTargetSafety.mjs) that both routes call beforefetch, and pins the connection to a validated IP via anundiciAgent so a DNS-rebinding race can't redirect the request after validation.Impact
Concretely, before this change, anyone who can reach the deployment can do:
…and have the Next.js server execute that fetch from inside its network boundary. On a non-2xx the response body is echoed back; on a 2xx the request still happens server-side (useful for triggering destructive POSTs against internal-only services, port-scanning by status/timing, etc.). Cloud metadata, internal Kubernetes services, intranet admin panels — anything the server's network reaches is in scope.
Severity: HIGH (CWE-918 SSRF, with CWE-350 / CWE-346 DNS rebinding mitigation as defense-in-depth).
Location
app/api/upload-binary/route.jsapp/api/v1/upload-binary/route.jsBoth files read
x-proxy-target-urlfrom the multipart form and callfetch(targetUrl, …)without any validation of scheme, host, or resolved IP.Fix
New helper
app/lib/proxyTargetSafety.mjsexportsvalidateAndPinProxyTarget(rawUrl). The route handlers call it beforefetch:The validator enforces, in order:
https://only (blockshttp://,file://,ftp://, …).https://attacker:secret@example.com/...).[::ffff:169.254.169.254]→[::ffff:a9fe:a9fe], the validator handles both), IPv4-compatible IPv6, IPv6 ULA (fc00::/7), IPv6 link-local (fe80::/10), IPv6 multicast (ff00::/8), and 2002::/16 6to4 wrapping a private IPv4.dns.lookup({ all: true }); if any returned record is in the same blocked ranges, the request is rejected.undiciAgentwith a customconnect.lookupso a racing DNS flip between the validator and the actualfetchcan't redirect the request to a private IP (canonical DNS-rebinding defense).PROXY_TARGET_ALLOWED_HOSTS(comma-separated host suffixes, e.g.s3.amazonaws.com,storage.googleapis.com) for deployments that want to scope the endpoint to a known storage-provider list.undiciis added as an explicit dependency (Node 18+ ships it internally but doesn't expose it as a built-in module).Detected by
Aeon + manual review of
app/api/*/route.js.Verification
tests/proxyTargetSafety.test.mjscovering every IPv4/IPv6 range, IPv4-mapped IPv6 in both forms, 6to4, DNS rebinding, allowlist behavior, malformed input, and the happy path.node --test tests/*.test.js tests/*.test.mjs→ 41/41 pass (12 pre-existing localInference tests + 29 new SSRF tests). No regressions.https://*.s3.amazonaws.com/...presigned URLs round-trip through unchanged; only the SSRF surface is closed.Notes for maintainers
{ url, dispatcher }; both routes passdispatcher: target.dispatchertofetch. This works in Next.js Route Handlers (Node.js runtime) because Node's globalfetchaccepts undici dispatchers. If you ever move these routes to the Edge runtime, the dispatcher path will need a different shape — flagging it here as a forward note rather than a current issue.PROXY_TARGET_ALLOWED_HOSTSis opt-in (undefined → all public hosts allowed). If you want stricter behavior in production, setting it tos3.amazonaws.com(and whatever storage hostname MuAPI actually hands back) tightens this further without code changes.Filed by Aeon.