Node.js SSRF Protection — Complete Guide to Server-Side Request Forgery Prevention
In this tutorial, you will learn about Node.js SSRF Protection. We cover key concepts, practical examples, and best practices to help you master this topic.
Server-Side Request Forgery (SSRF) attacks trick Node.js servers into making requests to internal resources, bypassing firewalls to access cloud metadata, databases, and internal services.
What You'll Learn
By the end of this tutorial, you'll implement SSRF protection through URL validation, IP allowlisting, DNS rebinding prevention, request timeouts, and secure HTTP client configuration.
Why SSRF Matters
SSRF is a critical OWASP vulnerability that exposes internal infrastructure. Cloud metadata endpoints at 169.254.169.254 are common targets. A single SSRF can leak cloud provider credentials.
Real-World Use
A URL preview service extracts Open Graph data from user-submitted URLs. Without SSRF protection, an attacker submits http://169.254.169.254/latest/meta-data/ to steal AWS credentials.
SSRF Protection Path
flowchart LR
A[Security Checklist] --> B[SSRF Protection]
B --> C[Helmet/CORS]
B --> D[JWT/OAuth]
C --> E[Deployment]
B --> F{You Are Here}
style F fill:#f90,color:#fff
URL Validation
Validate and sanitize URLs before making requests. Block internal and private IP ranges.
const { URL } = require("node:url");
function isValidUrl(urlString) {
try {
const parsed = new URL(urlString);
if (!["http:", "https:"].includes(parsed.protocol)) return false;
if (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") return false;
if (parsed.hostname.endsWith(".local") || parsed.hostname.endsWith(".internal")) return false;
return true;
} catch {
return false;
}
}
console.log(isValidUrl("https://example.com/data")); // true
console.log(isValidUrl("http://localhost:3000/admin")); // false
console.log(isValidUrl("http://169.254.169.254/")); // false
IP Allowlisting
Resolve hostnames and check against an allowlist of permitted IP ranges.
const dns = require("node:dns");
const { netmask } = require("netmask");
const ALLOWED_RANGES = [
new netmask("93.184.216.0/24"), // example.com
new netmask("140.82.112.0/20"), // github.com
];
async function isAllowedHost(hostname) {
return new Promise((resolve) => {
dns.resolve4(hostname, (err, addresses) => {
if (err) return resolve(false);
const allowed = addresses.some((ip) =>
ALLOWED_RANGES.some((range) => range.contains(ip))
);
resolve(allowed);
});
});
}
DNS Rebinding Protection
Prevent DNS rebinding attacks by validating IPs both before and after connection.
const dns = require("node:dns");
const http = require("node:http");
async function safeFetch(url) {
const parsed = new URL(url);
const initialIps = await dns.promises.resolve4(parsed.hostname);
if (initialIps.some((ip) => isPrivateIP(ip))) {
throw new Error("Blocked private IP");
}
return new Promise((resolve, reject) => {
const req = http.get(url, { lookup: (host, opts, cb) => {
dns.lookup(host, { family: 4 }, (err, ip) => {
if (err) return cb(err);
if (isPrivateIP(ip)) return cb(new Error("Blocked DNS rebinding"));
cb(null, ip, 4);
});
}}, resolve);
req.on("error", reject);
req.setTimeout(5000, () => { req.destroy(); reject(new Error("Timeout")); });
});
}
function isPrivateIP(ip) {
return /^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.)/.test(ip);
}
Request Timeouts
Always set timeouts on outgoing requests to prevent hanging connections.
const http = require("node:http");
function fetchWithTimeout(url, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
const req = http.get(url, (res) => {
let data = "";
res.on("data", (chunk) => data += chunk);
res.on("end", () => resolve(data));
});
req.setTimeout(timeoutMs, () => {
req.destroy();
reject(new Error("Request timeout"));
});
req.on("error", reject);
});
}
Using Blocked IP Libraries
Use established libraries like ssrf-req-filter or ip-range-check for production SSRF protection.
const http = require("node:http");
const { BlockList } = require("node:net");
const blocklist = new BlockList();
blocklist.addAddress("127.0.0.1");
blocklist.addRange("10.0.0.0", "10.255.255.255");
blocklist.addRange("172.16.0.0", "172.31.255.255");
blocklist.addSubnet("192.168.0.0", 16);
blocklist.addSubnet("169.254.0.0", 16);
function isBlocked(ip) {
return blocklist.check(ip);
}
Common Mistakes
1. Only Checking URL in Frontend
Frontend validation is bypassed easily. Always validate URLs on the server side.
2. Not Resolving DNS at Request Time
DNS rebinding changes IP resolution between validation and request. Resolve at request time.
3. Allowing file:// or gopher:// Protocols
SSRF Attacks use non-HTTP protocols. Only allow http and https.
4. No Timeout on Outbound Requests
Attackers can make your server hang indefinitely. Always set timeouts.
5. Trusting URL Parsing for Validation
URLs can contain obfuscated IPs, unicode domains, or redirect chains. Validate after normalization.
Practice Questions
1. What is SSRF?
Server-Side Request Forgery: tricking a server into making requests to internal or restricted resources.
2. Why is 169.254.169.254 a common SSRF target?
It is the cloud metadata endpoint for AWS, GCP, and Azure, containing instance credentials.
3. What is DNS rebinding?
An attack where a domain resolves to a benign IP first, then switches to a malicious IP after validation.
4. How do you prevent DNS rebinding?
Validate the IP both before the connection and at connection time, rejecting private IPs.
5. Challenge: Implement a URL fetch function with complete SSRF protection.
async function fetchUrl(url, options = {}) {
const parsed = new URL(url);
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("Invalid protocol");
if (isPrivateHost(parsed.hostname)) throw new Error("Blocked private host");
const { lookup } = require("node:dns").promises;
const [ip] = await lookup(parsed.hostname, { family: 4 });
if (isPrivateIP(ip)) throw new Error("Blocked private IP");
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), options.timeout || 5000);
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeout);
return response;
}
FAQ
Mini Project: SSRF-Protected URL Fetcher
Build a URL fetcher middleware with comprehensive SSRF protection.
const { BlockList } = require("node:net");
const dns = require("node:dns");
const blocklist = new BlockList();
[["127.0.0.0", 8], ["10.0.0.0", 8], ["172.16.0.0", 12], ["192.168.0.0", 16], ["169.254.0.0", 16]]
.forEach(([addr, subnet]) => blocklist.addSubnet(addr, subnet));
const http = require("node:http");
class SafeFetcher {
async get(url, timeout = 5000) {
const parsed = new URL(url);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("Unsupported protocol");
const [ip] = await dns.promises.resolve4(parsed.hostname);
if (blocklist.check(ip)) throw new Error("Blocked IP");
return new Promise((resolve, reject) => {
const req = http.get(url, { lookup: (h, o, cb) => cb(null, ip, 4) }, resolve);
req.setTimeout(timeout, () => { req.destroy(); reject(new Error("Timeout")); });
req.on("error", reject);
});
}
}
What's Next
Node.js GraphQL Node.js WebSocket Node.js JWT Authentication
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro