Inside shuvonsec/nextjs-ssrf-poc: the Next.js image optimizer trap that turns redirects into data leaks
A tiny local lab shows how an allowlisted image URL, a trusted redirect, and BMP passthrough combine into a much nastier SSRF than a blocked request.
- Next.js image allowlists are only as strong as the code that follows redirects.
- A trusted redirect can move the request into an internal zone without tripping the original host check.
- BMP passthrough turns a blind fetch into readable exfiltration because the optimizer preserves bytes it would normally strip.
- The repo's small three-server lab makes a framework-level trust failure legible in one pass.
Most SSRF demos end at the blocked request. This one starts where the guardrail says yes. Next.js approves the source host, follows a redirect it did not re-evaluate, then hands the result to an image pipeline that can preserve more bytes than a normal re-encode would.
Why the redirect defeats the allowlist
The important detail is timing. The allowlist in next.config.mjs says which source host is acceptable when the request begins, not where the request is allowed to end up after the server follows redirects. That is why this PoC is interesting. The front door looks strict, but the back half of the journey still moves.
export default {
images: {
remotePatterns: [{ protocol: 'http', hostname: 'localhost', port: '4444' }],
dangerouslyAllowLocalIP: true,
},
}
In other words, the policy is written for the first hop, while the exploit lives in the second. The mock redirect server sits inside the approved zone, then hands the optimizer a new destination that was never part of the original trust decision.
The BMP trick turns blind SSRF into exfiltration
A plain SSRF proof usually stops at reachability. This repo goes further by choosing a response type that can preserve raw bytes. The internal service returns a valid BMP header and appends extra data after the image body, so the optimizer can end up relaying more than just pixels.
const bmpHeader = Buffer.from([
0x42, 0x4d,
/* valid 1x1 BMP header bytes */
])
const hiddenTail = Buffer.from('{"secret":"redacted"}')
res.setHeader('Content-Type', 'image/bmp')
res.end(Buffer.concat([bmpHeader, pixelData, hiddenTail]))
That is the real upgrade. A blind server-side request becomes a response that can smuggle bytes back to the browser. The attack is no longer just "can the server reach it?" It becomes "can the server be made to return it intact?"
The lab is tiny on purpose
The repository is built like a miniature network diagram, not a full application. That is the point. Each file represents one trust zone, and the orchestration script makes the whole chain repeatable enough to inspect without guesswork.
app/, the minimal Next.js front end that exposes the image endpoint.next.config.mjs, the allowlist that defines the approved source host.redirect-server.mjs, the trusted-looking hop that issues the redirect.internal-service.mjs, the service that returns a valid BMP plus extra bytes.listener-server.mjs, the observer that helps verify what actually came back.run-poc.sh, the shell wrapper that boots the lab and reproduces the chain.
Why this PoC stands apart from simpler SSRF demos
| Case | Entry point | Trust check | Where the request ends up | Why it matters |
|---|---|---|---|---|
| Direct SSRF attempt | Image request | Blocks or rejects the URL up front | Never leaves the approved boundary | Useful as a baseline, but not the interesting failure mode. |
| Allowlisted redirect bypass | Allowed image host | Checks only the first hop | Follows a redirect to a forbidden target | Shows that validation can be correct and still be too early. |
| BMP passthrough exfiltration | Allowed host plus redirect | Trust decision happens before the body is normalized | Hidden bytes survive back to the browser | Turns reachability into visible data leakage. |
That is why this repository is more than a one-off bug demo. It is a compact lesson in how modern frameworks can fail at the handoff between validation, transport, and response handling. The code is small, but the security story is not.
What the repo teaches framework authors
Security controls that only inspect the first hop are fragile by default. Redirect handling, body normalization, and format-specific shortcuts all deserve the same scrutiny as the initial allowlist. This PoC is useful because it makes that failure mode concrete, reproducible, and hard to dismiss as theory.