Overview
SSRF vulnerabilities in Node.js (Express) can allow an attacker to coerce your server into making requests to internal services, cloud metadata endpoints, or other protected resources. This often occurs when an endpoint accepts a user-supplied URL and uses a fetch/HTTP client to proxy the request. The attacker may obtain sensitive data, access internal networks, or perform lateral movement from the compromised server.
In Express applications, SSRF most commonly appears in routes that read a URL from req.query or req.body and then fetch that URL. If there is no validation or network isolation, the server effectively becomes a proxy that can reach private IP addresses, metadata services, or other services behind a firewall, enabling information disclosure or lateral movement.
Without proper controls, this class of vulnerability can be exploited at scale by automated scanners, potentially compromising credentials or sensitive endpoints. While this guide does not reference CVEs, the pattern is well-known across Node.js/Express deployments and can be mitigated by strict input validation, allowlists, and network controls.
Code Fix Example
Node.js (Express) API Security Remediation
Vulnerable:
const express = require('express');
const app = express();
app.get('/fetch', async (req, res) => {
const url = req.query.url;
const resp = await fetch(url);
const data = await resp.text();
res.send(data);
});
Fixed:
const express = require('express');
const app = express();
const ALLOWED_HOSTS = new Set(['example.com','assets.example.com']);
const net = require('net');
function isPrivateIp(host) {
if (!net.isIP(host)) return false;
const parts = host.split('.').map(Number);
if (parts.length !== 4) return false;
const [a,b,c,d] = parts;
if (a === 10) return true;
if (a === 172 && b >= 16 && b <= 31) return true;
if (a === 192 && b === 168) return true;
return false;
}
app.get('/fetch', async (req, res) => {
const url = req.query.url;
try {
const parsed = new URL(url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') throw new Error('Unsupported protocol');
const host = parsed.hostname;
if (host === 'localhost' || host === '127.0.0.1' || isPrivateIp(host)) throw new Error('Host not allowed');
if (ALLOWED_HOSTS.size > 0 && !(ALLOWED_HOSTS.has(host) || ALLOWED_HOSTS.has(parsed.host))) throw new Error('Host not allowed');
const resp = await fetch(url, { timeout: 5000 });
const data = await resp.text();
res.send(data);
} catch (err) {
res.status(400).send({ error: err.message });
}
});