Next.js SSRF Protection
Server-Side Request Forgery (SSRF) happens when an attacker tricks your server into making requests to unintended destinations. In Next.js, this risk exists anywhere server-side code uses a URL or hostname from user input.
Where SSRF Happens in Next.js
Server Components
// app/preview/page.tsx — VULNERABLE
export default async function PreviewPage({ searchParams }: { searchParams: { url?: string } }) {
const url = searchParams.url ?? "";
// Attacker can set url=http://169.254.169.254/latest/meta-data/
const res = await fetch(url);
const data = await res.text();
return <div>{data}</div>;
}
Server Actions
// VULNERABLE
"use server";
export async function fetchPreview(url: string) {
const res = await fetch(url); // user controls the target
return res.text();
}
Route Handlers
// app/api/proxy/route.ts — VULNERABLE
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const target = searchParams.get("target") ?? "";
const res = await fetch(target); // open proxy
return new Response(await res.text());
}
Image Optimization
Next.js <Image> with external sources can be an SSRF vector if remotePatterns is too permissive.
// next.config.ts — too permissive
const nextConfig = {
images: {
remotePatterns: [{ protocol: "https", hostname: "**" }], // allows ANY host
},
};
Reference: Next.js — Image Optimization
Defences
1. Allowlist Domains
The most effective defence. Only allow requests to known, trusted domains:
const ALLOWED_HOSTS = new Set([
"api.example.com",
"cdn.example.com",
]);
function validateUrl(input: string): URL {
const url = new URL(input);
if (!ALLOWED_HOSTS.has(url.hostname)) {
throw new Error("Host not allowed");
}
if (url.protocol !== "https:") {
throw new Error("HTTPS required");
}
return url;
}
2. Block Private IP Ranges
If you cannot use a strict allowlist, block internal addresses:
import { isIP } from "net";
import dns from "dns/promises";
const PRIVATE_RANGES = [
/^127\./,
/^10\./,
/^172\.(1[6-9]|2\d|3[01])\./,
/^192\.168\./,
/^169\.254\./,
/^0\./,
/^::1$/,
/^fc00:/i,
/^fe80:/i,
/^fd/i,
];
function isPrivateIp(ip: string): boolean {
return PRIVATE_RANGES.some((r) => r.test(ip));
}
async function validateNotInternal(hostname: string): Promise<void> {
if (isIP(hostname)) {
if (isPrivateIp(hostname)) {
throw new Error("Private IP blocked");
}
return;
}
// Resolve both IPv4 and IPv6 to prevent bypass via AAAA records
const [v4, v6] = await Promise.all([
dns.resolve4(hostname).catch(() => []),
dns.resolve6(hostname).catch(() => []),
]);
for (const addr of [...v4, ...v6]) {
if (isPrivateIp(addr)) {
throw new Error("Resolved to private IP");
}
}
}
3. Disable Redirects
Attackers use open redirects to bypass hostname checks. The initial URL passes validation, then redirects to an internal address:
const res = await fetch(validatedUrl, {
redirect: "error", // throw on any redirect
});
4. Restrict Next.js Image Domains
Lock down remotePatterns to specific hosts:
// next.config.ts
const nextConfig = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "cdn.example.com" },
{ protocol: "https", hostname: "avatars.githubusercontent.com" },
],
},
};
Reference: Next.js — remotePatterns
5. Network-Level Controls
- In AWS, enforce IMDSv2 (requires a token, blocks SSRF to metadata): AWS — IMDSv2
- In GCP, use the metadata concealment endpoint or workload identity.
- Use VPC security groups and firewall rules to restrict outbound traffic from application servers.
- Run applications in containers with restricted network policies.
6. Timeout and Size Limits
Even with validation, set conservative limits:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const res = await fetch(validatedUrl, {
signal: controller.signal,
redirect: "error",
});
clearTimeout(timeout);
// Limit response size
const MAX_SIZE = 1024 * 1024; // 1 MB
const body = await res.text();
if (body.length > MAX_SIZE) {
throw new Error("Response too large");
}
Cloud Metadata Endpoints to Block
| Cloud | Metadata URL |
|---|---|
| AWS | http://169.254.169.254/latest/meta-data/ |
| GCP | http://metadata.google.internal/computeMetadata/ |
| Azure | http://169.254.169.254/metadata/instance |
| DigitalOcean | http://169.254.169.254/metadata/v1/ |
Reference: AWS — Instance Metadata
Related Guides
Content is AI-assisted and reviewed by our team, but issues may be missed and best practices evolve rapidly, send corrections to [email protected]. Always consult official documentation and validate key implementation decisions before making design or security choices.
