flawopen.com/Server-Side Request Forgery/JavaScript
You give a delivery bot a delivery address. The bot is allowed inside the building. If the user tells the bot 'deliver to 127.0.0.1:6379', the bot walks straight to your Redis database and speaks raw commands.
In Node.js, libraries like axios, node-fetch, and native fetch resolve hostnames and connect without inspecting the destination IP. Attackers use this to query internal Kubernetes pods, Redis instances, or cloud metadata.
In 2023, numerous webhooks and PDF generation microservices in Node.js allowed attackers to access local Docker daemon sockets (http://localhost:2375) and achieve full host container escape.
// VULNERABLE: Naively fetching user URL with axios
const express = require('express');
const axios = require('axios');
const app = express();
app.get('/proxy-image', async (req, res) => {
const imageUrl = req.query.url;
// Attacker input: http://169.254.169.254/latest/meta-data/
try {
const response = await axios.get(imageUrl);
res.send(response.data);
} catch (err) {
res.status(500).send('Fetch error');
}
});
// HARDENED: Custom DNS lookup agent blocking private & metadata IP ranges
const express = require('express');
const http = require('http');
const https = require('https');
const dns = require('dns');
const ipaddr = require('ipaddr.js'); // npm i ipaddr.js
const axios = require('axios');
const app = express();
function safeLookup(hostname, options, callback) {
dns.lookup(hostname, options, (err, address, family) => {
if (err) return callback(err);
try {
const addr = ipaddr.parse(address);
const range = addr.range();
// Deny private, loopback, link-local, and reserved ranges
const blockedRanges = ['loopback', 'private', 'linkLocal', 'carrierGradeNat', 'reserved'];
if (blockedRanges.includes(range)) {
return callback(new Error(`SSRF Blocked: Destination IP ${address} is in blocked range (${range})`));
}
callback(null, address, family);
} catch (parseErr) {
callback(new Error('Invalid IP address resolved'));
}
});
}
const safeHttpAgent = new http.Agent({ lookup: safeLookup });
const safeHttpsAgent = new https.Agent({ lookup: safeLookup });
app.get('/proxy-image', async (req, res) => {
const imageUrl = req.query.url;
try {
const parsed = new URL(imageUrl);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return res.status(400).send('Invalid protocol');
}
const response = await axios.get(imageUrl, {
httpAgent: safeHttpAgent,
httpsAgent: safeHttpsAgent,
maxRedirects: 0, // Prevent redirect bypasses
timeout: 3000
});
res.send(response.data);
} catch (err) {
res.status(403).send('Request blocked: ' + err.message);
}
});
The custom lookup function in Node's HTTP/HTTPS Agent intercepts DNS resolution at the socket level. Before the TCP handshake begins, ipaddr.js evaluates the resolved IP. If it falls into loopback, private, or link-local ranges, the socket is aborted immediately, completely preventing DNS rebinding.
Global fetch() in Node.js 18+ uses undici, which handles DNS differently. Use an explicit Dispatcher or a validated library like axios with custom agent.
Hostnames like 'localtest.me' or 'customer.com' can point to 127.0.0.1 or 169.254.169.254. DNS resolution inspection is mandatory.
npm audit
npx eslint --plugin security
lookup callbackipaddr.jsmaxRedirects: 0 to disallow 302 redirects to internal endpointsYes, if IPv6 is not handled. Addresses like ::1, ::ffff:127.0.0.1, or fc00::/7 are private. ipaddr.js handles both IPv4 and IPv6.