SSRF in Next.js Applications
Next.js runs server-side code in API routes, Server Actions, and Server Components, making it a prime target for SSRF attacks. When these server-side contexts fetch URLs supplied by users — such as webhook endpoints, image URLs for previews, or link unfurling — attackers can pivot to internal services, cloud metadata endpoints, and private network resources.
Scan Your Next.js AppHow SSRF Manifests in Next.js
SSRF in Next.js most commonly appears in API routes that accept a URL parameter and fetch it server-side. Examples include link preview generators, URL shorteners, screenshot services, and webhook delivery endpoints. The server-side fetch() in these routes can reach internal services that are not accessible from the public internet. Server Actions introduced in App Router create a new SSRF surface. A Server Action that accepts a URL from form data and fetches it runs on the server with full network access. Attackers can submit forms targeting internal APIs, cloud metadata services (169.254.169.254), or localhost endpoints. The Next.js Image Optimization API (next/image) can also be exploited if the remotePatterns configuration is too permissive, allowing attackers to use the server as a proxy to fetch images from internal hosts. Server Components that fetch data based on URL search parameters or dynamic route segments are another vector. Since Server Components execute exclusively on the server, any fetch() call with user-controlled URLs has SSRF potential.
Real-World Impact
An attacker submits a webhook URL pointing to http://169.254.169.254/latest/meta-data/iam/security-credentials/ through a Next.js API route that validates webhooks by sending a test request. The server fetches the AWS metadata endpoint, returning temporary IAM credentials. The attacker uses these credentials to access S3 buckets, databases, and other AWS resources. In another scenario, a Next.js app with a link preview feature fetches user-submitted URLs to extract Open Graph metadata. An attacker provides http://localhost:3000/api/admin/users, causing the server to request its own admin API endpoint, bypassing authentication since the request comes from localhost.
Step-by-Step Fix
Create a URL validation utility
Build a reusable function that validates URLs against an allowlist and blocks internal addresses.
// lib/url-validator.ts
import { URL } from 'url';
import dns from 'dns/promises';
import { isIP } from 'net';
const BLOCKED_RANGES = [
/^127\./, /^10\./, /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./,
/^169\.254\./, /^0\./, /^::1$/, /^fc00:/, /^fe80:/,
];
export async function validateExternalUrl(input: string): Promise<URL> {
const url = new URL(input);
if (![ 'http:', 'https:'].includes(url.protocol)) {
throw new Error('Only HTTP(S) URLs are allowed');
}
// Resolve DNS to check actual IP
const hostname = url.hostname;
const ips = isIP(hostname)
? [hostname]
: (await dns.resolve4(hostname).catch(() => []));
for (const ip of ips) {
if (BLOCKED_RANGES.some(r => r.test(ip))) {
throw new Error('Requests to internal addresses are blocked');
}
}
return url;
}Secure API routes that fetch user URLs
Apply the URL validator in every API route that performs server-side requests based on user input.
// app/api/preview/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { validateExternalUrl } from '@/lib/url-validator';
export async function POST(req: NextRequest) {
const { url } = await req.json();
try {
const validatedUrl = await validateExternalUrl(url);
const response = await fetch(validatedUrl.toString(), {
signal: AbortSignal.timeout(5000),
redirect: 'error', // Block redirects to internal URLs
});
const html = await response.text();
// Extract metadata safely...
return NextResponse.json({ title: extractTitle(html) });
} catch (err) {
return NextResponse.json({ error: 'Invalid URL' }, { status: 400 });
}
}Lock down next/image remote patterns
Configure remotePatterns in next.config.js to only allow specific trusted image domains.
// next.config.js
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
},
],
// Never use: domains: ['*'] or overly broad patterns
},
};
module.exports = nextConfig;Protect Server Actions from SSRF
Validate URLs received in Server Actions before making any server-side requests.
// app/actions.ts
'use server';
import { validateExternalUrl } from '@/lib/url-validator';
export async function submitWebhook(formData: FormData) {
const webhookUrl = formData.get('url') as string;
// Validate before storing or testing
try {
await validateExternalUrl(webhookUrl);
} catch {
return { error: 'Invalid webhook URL. Only public HTTPS URLs are allowed.' };
}
// Safe to store and later deliver to this URL
await db.webhooks.create({ url: webhookUrl });
return { success: true };
}Prevention Best Practices
1. Validate and allowlist URLs before fetching. Only permit specific protocols (https:) and domains. 2. Block requests to private IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x) and localhost. 3. Use DNS resolution validation to prevent DNS rebinding attacks where a domain resolves to an internal IP. 4. Configure next/image remotePatterns with specific hostnames rather than wildcards. 5. Set timeouts and size limits on server-side fetch requests to prevent resource exhaustion. 6. Run Next.js in a network segment that cannot reach sensitive internal services.
How to Test
1. Submit http://169.254.169.254/latest/meta-data/ as a URL parameter in any endpoint that fetches URLs and check if cloud metadata is returned. 2. Try http://localhost:3000/api/admin or http://127.0.0.1:3000 to test for internal service access. 3. Test DNS rebinding by using a domain that alternates between a public IP and 127.0.0.1. 4. Check next.config.js for overly broad remotePatterns or domains configurations in the image optimizer. 5. Use Vibe App Scanner to automatically detect SSRF vectors in your Next.js application.
Frequently Asked Questions
Can Next.js Server Components be exploited for SSRF?
Yes. Server Components run exclusively on the server and have full network access. If a Server Component uses a URL derived from search parameters or dynamic route segments to make a fetch() call, an attacker can manipulate the URL to target internal services, cloud metadata endpoints, or localhost.
Does the Next.js image optimizer prevent SSRF?
The image optimizer respects the remotePatterns configuration in next.config.js, but an overly permissive configuration (like allowing all hostnames) turns it into an SSRF proxy. Always restrict remotePatterns to specific trusted domains and paths.
How does SSRF differ from open redirect in Next.js?
SSRF causes the server to make a request to an attacker-controlled destination, potentially exposing internal resources. Open redirect causes the client browser to navigate to a malicious URL. SSRF is server-side and can access internal infrastructure, while open redirect is client-side and primarily used for phishing.
Related Security Resources
Is Your Next.js App Vulnerable to SSRF?
VAS automatically scans for ssrf vulnerabilities in Next.js applications and provides step-by-step remediation guidance with code examples.
Scans from $5, results in minutes. Get actionable fixes tailored to your Next.js stack.