How to Avoid: SSRF via User-Controlled URL
August 6, 2026 · 8-minute read · Fairy
The short answer
To prevent SSRF via user-controlled URLs in AI-generated code, implement a strict allowlist of permitted domains, block requests to private IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x), resolve hostnames before fetching to prevent DNS rebinding, and validate URL schemes to permit only HTTPS. Never trust AI-generated fetch logic without these safeguards.
How to Avoid: SSRF via User-Controlled URL
When AI generates code that fetches a URL provided by a user, it almost always creates a Server-Side Request Forgery (SSRF) vulnerability. The AI writes the happy path—take URL, fetch URL, return response—without recognizing that your server has network access the user shouldn't inherit.
The fix requires three layers: allowlist permitted destinations, block private IP ranges before fetching, and resolve hostnames to prevent DNS rebinding. URL string validation alone is insufficient.
What Is SSRF and Why AI Creates It
Server-Side Request Forgery happens when an attacker supplies a URL that your server fetches, but the destination is something internal—your database, a cloud metadata endpoint, an admin service, or another host on your private network.
Your server sits inside your infrastructure. It can reach things the internet cannot: http://localhost:6379 (Redis), http://169.254.169.254/latest/meta-data/ (AWS instance credentials), http://internal-api.corp:8080/admin. When you fetch a user-supplied URL, you're effectively giving users your server's network position.
AI models generate SSRF-vulnerable code because they're trained on examples that prioritize functionality:
// AI-generated: VULNERABLE
app.post('/fetch-url', async (req, res) => {
const { url } = req.body;
const response = await fetch(url);
const data = await response.text();
res.send(data);
});
This code works. It does exactly what was asked. But the AI has no concept that url might be http://169.254.169.254/latest/meta-data/iam/security-credentials/ and that your AWS instance role credentials would be returned to the attacker.
This pattern mirrors other AI security gaps we see frequently. Just as AI-generated code often bypasses authentication via query parameters—granting admin access when req.query.admin === "true" because it satisfies the functional requirement without considering adversarial input—SSRF vulnerabilities emerge because the AI solves the stated problem without modeling the threat.
Why URL Validation Alone Fails
The intuitive fix—check that the URL looks safe—doesn't work against determined attackers:
// INSUFFICIENT: String-based validation
function isUrlSafe(url) {
const parsed = new URL(url);
if (parsed.hostname === 'localhost') return false;
if (parsed.hostname.startsWith('192.168.')) return false;
return true;
}
Attackers bypass this with:
- DNS rebinding: Register a domain that initially resolves to a safe IP, then changes to 127.0.0.1 between your validation check and the actual fetch
- URL parsing inconsistencies: Different parsers interpret URLs differently;
http://google.com@169.254.169.254/might parse as google.com in validation but fetch from the metadata IP - Redirect chains: The URL points to an external server that returns a 302 redirect to an internal address
- IPv6 representations:
http://[::1]/is localhost,http://[::ffff:127.0.0.1]/maps to 127.0.0.1 - Decimal/octal IP notation:
http://2130706433/is 127.0.0.1 in decimal
String-based URL validation is necessary but nowhere near sufficient.
The Correct Pattern: Defense in Depth
Preventing SSRF requires multiple layers that work together:
Layer 1: Allowlist Permitted Destinations
If your feature only needs to fetch from specific domains, enforce that at the application level:
const ALLOWED_DOMAINS = new Set([
'api.example.com',
'cdn.trusted-partner.com',
'images.your-service.com'
]);
function isDomainAllowed(url) {
try {
const parsed = new URL(url);
return ALLOWED_DOMAINS.has(parsed.hostname);
} catch {
return false;
}
}
An allowlist is the strongest control. If you can enumerate the valid destinations, do so.
Layer 2: Block Private IP Ranges
When you can't use an allowlist (user provides arbitrary URLs), you must block private IP ranges. Critically, check the resolved IP, not the hostname:
import dns from 'dns/promises';
import { isIP } from 'net';
const PRIVATE_IP_RANGES = [
/^127\./, // Loopback
/^10\./, // Private Class A
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Private Class B
/^192\.168\./, // Private Class C
/^169\.254\./, // Link-local (cloud metadata)
/^0\./, // Current network
/^100\.(6[4-9]|[7-9][0-9]|1[0-2][0-7])\./, // Carrier-grade NAT
/^::1$/, // IPv6 loopback
/^fc00:/i, // IPv6 unique local
/^fe80:/i, // IPv6 link-local
];
function isPrivateIP(ip) {
return PRIVATE_IP_RANGES.some(range => range.test(ip));
}
async function resolveAndValidate(url) {
const parsed = new URL(url);
// Only allow HTTPS
if (parsed.protocol !== 'https:') {
throw new Error('Only HTTPS URLs are permitted');
}
// Resolve the hostname to an IP
const addresses = await dns.resolve4(parsed.hostname);
// Check ALL resolved IPs (some domains return multiple)
for (const ip of addresses) {
if (isPrivateIP(ip)) {
throw new Error('URL resolves to private IP range');
}
}
return parsed;
}
Layer 3: Prevent DNS Rebinding
The time between resolving a hostname and actually connecting creates a window for DNS rebinding attacks. Pin the resolved IP and connect to it directly:
import https from 'https';
async function safeFetch(url) {
const parsed = await resolveAndValidate(url);
// Resolve again and pin the IP
const addresses = await dns.resolve4(parsed.hostname);
const pinnedIP = addresses[0];
// Verify the pinned IP is still safe (defense in depth)
if (isPrivateIP(pinnedIP)) {
throw new Error('DNS rebinding detected');
}
return new Promise((resolve, reject) => {
const options = {
hostname: pinnedIP,
port: 443,
path: parsed.pathname + parsed.search,
method: 'GET',
headers: {
'Host': parsed.hostname // Original hostname for SNI/Host header
},
// Prevent redirect following (handle manually if needed)
maxRedirects: 0
};
const req = https.request(options, (res) => {
// Handle response
});
req.on('error', reject);
req.end();
});
}
Layer 4: Handle Redirects Carefully
If you must follow redirects, validate each hop:
async function safeFetchWithRedirects(url, maxRedirects = 5) {
let currentUrl = url;
let redirectCount = 0;
while (redirectCount < maxRedirects) {
const parsed = await resolveAndValidate(currentUrl);
const response = await fetchWithoutRedirects(currentUrl);
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location');
if (!location) break;
// Resolve relative URLs against current URL
currentUrl = new URL(location, currentUrl).toString();
redirectCount++;
continue;
}
return response;
}
throw new Error('Too many redirects');
}
Complete Implementation Example
Here's a production-ready module that combines all layers:
// ssrf-safe-fetch.js
import dns from 'dns/promises';
import https from 'https';
import http from 'http';
const ALLOWED_PROTOCOLS = new Set(['https:']);
const MAX_REDIRECTS = 5;
const BLOCKED_IP_PATTERNS = [
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
/^192\.168\./,
/^169\.254\./,
/^0\./,
/^100\.(6[4-9]|[7-9][0-9]|1[0-2][0-7])\./,
];
class SSRFError extends Error {
constructor(message) {
super(message);
this.name = 'SSRFError';
}
}
function isBlockedIP(ip) {
return BLOCKED_IP_PATTERNS.some(pattern => pattern.test(ip));
}
async function validateUrl(url, allowedDomains = null) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new SSRFError('Invalid URL format');
}
if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
throw new SSRFError(`Protocol ${parsed.protocol} not allowed`);
}
if (allowedDomains && !allowedDomains.has(parsed.hostname)) {
throw new SSRFError(`Domain ${parsed.hostname} not in allowlist`);
}
// Resolve and validate IP
let addresses;
try {
addresses = await dns.resolve4(parsed.hostname);
} catch {
throw new SSRFError('Could not resolve hostname');
}
for (const ip of addresses) {
if (isBlockedIP(ip)) {
throw new SSRFError('URL resolves to blocked IP range');
}
}
return { parsed, resolvedIP: addresses[0] };
}
export async function safeFetch(url, options = {}) {
const { allowedDomains = null, timeout = 10000 } = options;
const { parsed, resolvedIP } = await validateUrl(url, allowedDomains);
// Re-verify IP hasn't changed (DNS rebinding protection)
if (isBlockedIP(resolvedIP)) {
throw new SSRFError('DNS rebinding detected');
}
return new Promise((resolve, reject) => {
const requestOptions = {
hostname: resolvedIP,
port: parsed.port || 443,
path: parsed.pathname + parsed.search,
method: 'GET',
headers: { 'Host': parsed.hostname },
timeout,
};
const req = https.request(requestOptions, resolve);
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new SSRFError('Request timeout'));
});
req.end();
});
}
What to Check in AI-Generated Code
When reviewing AI-generated code that handles user-supplied URLs, verify:
- No direct fetch of user input: Look for
fetch(userUrl),axios.get(userUrl),http.get(userUrl)patterns - IP resolution before fetch: The hostname must be resolved and the IP validated before any connection
- Private IP blocking: All RFC 1918 ranges, loopback, link-local, and cloud metadata IPs must be blocked
- Protocol restriction: Only HTTPS should be permitted (HTTP allows MITM and reaches more internal services)
- Redirect handling: Either disable redirects or validate each redirect destination
This vulnerability class is exactly what automated AI code review catches—patterns that satisfy the prompt but create security exposure.
Network-Level Defenses
Code-level controls should be complemented by infrastructure:
- Egress filtering: Configure firewalls to block outbound connections from application servers to internal networks
- Metadata endpoint protection: AWS IMDSv2 requires session tokens, making accidental exposure harder
- Dedicated fetch services: Route external URL fetches through a locked-down service with no internal network access
- DNS resolution isolation: Use a resolver that refuses to return private IPs for external queries
Testing Your SSRF Defenses
Before deploying, test with:
# Cloud metadata endpoints
http://169.254.169.254/latest/meta-data/
http://metadata.google.internal/computeMetadata/v1/
# Internal network
http://localhost:8080/admin
http://127.0.0.1:6379/
# IP encoding variations
http://2130706433/ # 127.0.0.1 in decimal
http://0x7f000001/ # 127.0.0.1 in hex
http://[::1]/ # IPv6 loopback
# Redirect-based bypass (requires external server)
https://your-test-server.com/redirect?to=http://169.254.169.254/
Your application should reject all of these.
Why This Matters for AI-Generated Code
AI models will continue generating direct-fetch patterns because that's what training data shows for "fetch a URL" tasks. The security considerations require threat modeling that current AI doesn't perform.
When you use AI to generate code involving user-controlled URLs, treat the output as a first draft that needs security review. The pattern AI generates will work—it just won't be safe.
For organizations deploying AI-generated code at scale, this is exactly why verification infrastructure exists. SSRF is one of many vulnerability classes where AI produces functional but exploitable code, and catching these patterns before production is what separates reliable AI deployment from security incidents.
Frequently asked questions
What is SSRF and why is it dangerous?
Server-Side Request Forgery (SSRF) occurs when an attacker tricks a server into making requests to unintended locations. This can expose internal services, cloud metadata endpoints (like AWS 169.254.169.254), and private network resources that should never be accessible from the internet.
Why does AI-generated code often contain SSRF vulnerabilities?
AI models optimize for functionality over security. When asked to fetch a URL provided by a user, AI typically generates the most direct implementation—passing the URL straight to a fetch function—without considering that the server's network position grants access to internal resources the user shouldn't reach.
Is URL validation enough to prevent SSRF?
No. URL validation alone is insufficient because attackers can use DNS rebinding, URL parsing inconsistencies, or redirect chains to bypass string-based checks. You must resolve the hostname, verify the resolved IP is not in private ranges, and follow redirects carefully.
What IP ranges should I block to prevent SSRF?
Block all private and reserved IP ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16 (link-local/cloud metadata), 0.0.0.0/8, and IPv6 equivalents like ::1 and fc00::/7.
How do I test for SSRF vulnerabilities in my code?
Test by submitting URLs pointing to internal services (http://localhost, http://169.254.169.254), private IPs, and domains you control that resolve to private IPs. Use DNS rebinding tools to verify your resolution-time checks work correctly.
Have AI-generated work you’d want verified? Connect with a Fairy → or run a free check with Scout.
More resources