Overview
SSRF vulnerabilities in Node.js Express apps allow attackers to induce the server to perform requests to arbitrary destinations. In real deployments, this can expose internal services, cloud metadata endpoints, or admin interfaces, leading to data exfiltration, credential leakage, or unauthorized access to restricted resources.
These flaws typically occur when an Express route proxies or fetches a URL supplied by a client, using libraries such as fetch, axios, or request without strict validation. If the URL is passed directly to the outbound request, an attacker can reach internal networks, services behind firewalls, or metadata services, potentially bypassing network controls.
Although no CVEs are provided here, SSRF is a well-known risk across Node.js ecosystems. Risk is amplified by redirects, permissive allowlists, or lack of protocol/host validation, and by running in cloud environments where internal endpoints are available from the instance.
Remediation focuses on input validation and network controls: implement strict allowlists, validate and canonicalize URLs, block private/internal addresses, limit redirects and timeouts, route outbound traffic through a controlled gateway, and monitor for SSRF attempts with proper logging and alerting.
Code Fix Example
Node.js (Express) API Security Remediation
Vulnerable pattern:
```js
const express = require('express');
const app = express();
// Vulnerable: proxies user-provided URL directly
app.get('/proxy', async (req, res) => {
const url = req.query.url;
const r = await fetch(url);
const data = await r.text();
res.send(data);
});
```
Fixed pattern:
```js
const express = require('express');
const fetch = require('node-fetch');
const app = express();
const ALLOWED_ORIGINS = ['https://example.com','https://api.example.org'];
// Safe: validate and restrict outbound URL
app.get('/proxy', async (req, res) => {
const url = req.query.url;
try {
const parsed = new URL(url);
if (['http:', 'https:'].indexOf(parsed.protocol) === -1) {
return res.status(400).send('Unsupported protocol');
}
if (!ALLOWED_ORIGINS.includes(parsed.origin)) {
return res.status(400).send('URL not allowed');
}
// Block internal hosts
if (['localhost','127.0.0.1'].includes(parsed.hostname) || parsed.hostname.endsWith('.internal')) {
return res.status(400).send('Internal host not allowed');
}
const r = await fetch(url);
const data = await r.text();
res.send(data);
} catch (e) {
res.status(400).send('Invalid URL');
}
});
```