Overview
In production environments, SSRF can allow an attacker to coax your server into making requests to internal services, cloud metadata endpoints, or other restricted networks. This can reveal sensitive data, trigger internal port scans, or enable privilege escalation via side channels. In Node.js and Express apps, common mistakes include echoing back a user-supplied URL, proxy endpoints, or server-to-server calls based on user input.
In Express apps, SSRF often manifests when you fetch or fetch-like modules using a URL taken from req.query or req.body without validation. If the server is allowed to reach internal resources (for example, container metadata services, or private APIs), an attacker can reach those resources, sometimes bypassing access controls, since the request is made from trusted server context.
There are no CVEs provided in this guide; treat this as a general pattern-based vulnerability. Risks include data exposure, vault/secret leaks, or leaking internal endpoints. The impact depends on what internal resources are reachable from your VPC or cloud environment.
Mitigation involves validating and controlling outbound requests, adopting allow-lists for destinations, avoiding user-controlled URLs, using internal services for external requests, implementing network egress controls, and thorough testing (including fuzzing and dependency updates).
Code Fix Example
Node.js (Express) API Security Remediation
// Vulnerable pattern
const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.get('/proxy', async (req, res) => {
const url = req.query.url;
// SSRF: unvalidated user input used to fetch arbitrary URL
const r = await fetch(url);
const data = await r.text();
res.send(data);
});
// Safe variant (the fix) using a strict allow-list
const allowedHosts = ['example.com', 'internal-service.local'];
function isAllowedUrl(u) {
try {
const url = new URL(u);
return (url.protocol === 'http:' || url.protocol === 'https:') && allowedHosts.includes(url.hostname);
} catch (e) {
return false;
}
}
app.get('/proxy-safe', async (req, res) => {
const url = req.query.url;
if (!isAllowedUrl(url)) {
return res.status(400).send('Forbidden');
}
const r = await fetch(url);
const data = await r.text();
res.send(data);
});