no-ssrf
Detect HTTP requests with user-controlled URLs (server-side request forgery).
Detect outbound HTTP requests whose URL is derived from user-controlled input. An attacker can pivot the request at internal services (metadata endpoints, admin panels, databases) to bypass perimeter controls.
This rule is part of eslint-plugin-node-security.
Quick Summary
| Aspect | Details |
|---|---|
| Severity | Critical (Security — perimeter bypass) |
| Auto-Fix | 💡 Suggestions (URL allowlist scaffold) |
| Category | Node Security |
| CWE | CWE-918 |
| OWASP | A10:2021 |
| Best For | Any service that makes outbound HTTP and accepts user-provided URLs |
Why SSRF matters
A server inside a cloud VPC can typically reach:
- The cloud metadata service (
169.254.169.254) — AWS IMDS, GCP, Azure — where credentials live. - Other internal services (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16). - The loopback (
127.0.0.1) and Unix sockets.
If your server accepts a URL from a user request and fetches it, the user can borrow your server's identity to reach those targets. The 2019 Capital One breach was a textbook SSRF chain that exfiltrated 100M records via IMDS.
Examples
❌ Incorrect
app.get('/preview', async (req, res) => {
const r = await fetch(req.query.url); // attacker controls target
res.send(await r.text());
});
app.post('/import', async (req, res) => {
const r = await axios.get(req.body.feedUrl); // same exposure via axios
res.json(r.data);
});
const r = await fetch(`${API_BASE}${userPath}`); // path traversal lets attacker swap host✅ Correct
import { URL } from 'node:url';
const ALLOWED_HOSTS = new Set(['api.partner.example', 'cdn.partner.example']);
const BLOCKED_CIDRS = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '127.0.0.0/8', '169.254.0.0/16'];
function safeFetch(rawUrl) {
const url = new URL(rawUrl);
if (url.protocol !== 'https:') throw new Error('https only');
if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error('host not allowed');
// Resolve DNS before request; reject if the resolved IP sits in BLOCKED_CIDRS.
return fetch(url, { redirect: 'error' }); // refuse redirects to avoid TOCTOU
}✅ Also correct — no evidence of user flow
The rule reports on evidence, not on the mere presence of a dynamic argument. A Node options object whose fields are plain locals is internal plumbing, not a user-controlled URL:
function fetchProfile(host, path, cb) {
// host/path are this helper's own parameters — not flagged
return https.request({ host, path, method: 'GET' }, cb);
}
const r = await fetch(`https://api.internal/items/${id}`); // id is a localThree shapes count as evidence:
- The whole argument is a user-input-named identifier —
fetch(userUrl). - Any part of the expression reads off a request object (
req,request,ctx,event) —https.request({ host: req.query.host }). - A template literal or concatenation interpolating either of the above.
Error Message Format
🔒 NODE-SECURITY CWE-918 OWASP:A10 | SSRF: user-controlled URL passed to outbound request | CRITICAL
Fix: Validate the URL against an allowlist of trusted hosts, refuse redirects, and resolve DNS to block internal IPs (RFC 1918, link-local, loopback).Known False Negatives
- URL fragments derived from a chain of variables (more than one assignment hop) are not always traced.
- Constructed
URLobjects passed torequest.get(urlObject)are detected when the constructor receives user input directly, not when input enters via setter methods. - Allowlists declared in environment variables (consumed at runtime) cannot be validated statically — the rule still fires; suppress with an
eslint-disable-next-linecomment that names the validating function.
⚙️ Options
| Option | Type | Default | Description |
|---|---|---|---|
allowInTests | boolean | true | Skip this rule in *.test.* / *.spec.* files |
reportUnresolvedUrls | boolean | false | Report a URL argument that is a user-input-named identifier with no traceable request source. Restores the pre-inversion naming heuristic. |
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.