RSTR-SSRF-001 — fetch/axios with request input
Summary
A server-side HTTP request is built from request input that
the caller never validates. An attacker can substitute a URL
that points at internal infrastructure
(http://localhost:6379, http://169.254.169.254 cloud
metadata, internal admin panels) and either exfiltrate the
response or use the server as a confused-deputy proxy.
This is server-side request forgery (SSRF), CWE-918.
Severity
High. SSRF in cloud workloads can read IAM credentials
from the metadata endpoint, leading directly to privilege
escalation.
Languages
JavaScript, TypeScript (and their JSX / TSX / .mjs / .cjs variants).
What rastray flags
The first argument of fetch(...), axios.get(...),
axios.post(...), axios.put(...), axios.patch(...),
axios.delete(...), axios.head(...), axios.options(...),
or axios.request(...) is a direct property access on
req.body.*, req.query.*, req.params.*, req.cookies.*,
or req.headers.*.
True-positive example:
app.get('/proxy', async (req, res) => {
const r = await fetch(req.body.url); // ← flagged
res.send(await r.text());
});
const r = await axios.get(req.query.next); // ← flagged
What rastray deliberately does not flag
Indirect flow via a local variable. The rule requires the request expression to appear directly in the sink:
const url = req.body.url;
const r = await fetch(url); // ← not flagged
This is the same conservative scope used by every other rastray security rule. Multi-step taint flow is what CodeQL and Semgrep Pro do; rastray catches the common 80% where the dangerous value is right there in the call.
Literal URLs:
const r = await fetch('https://api.github.com/repos/x/y'); // ← not flagged
Validated values:
const safe = new URL(req.body.url);
if (!ALLOWED_HOSTS.has(safe.hostname)) throw new Error('blocked');
const r = await fetch(safe); // ← not flagged
Why the finding message looks the way it does
Every RSTR-SSRF-001 finding interpolates the matched call:
RSTR-SSRF-001:
fetch(req.body.url)issues an HTTP request to a URL taken from request input — SSRF risk
If you have 50 SSRF findings in one report, all 50 messages are distinguishable because each shows the exact call that matched, not a generic template.
How to fix it
Option 1 — allow-list the host:
const ALLOWED_HOSTS = new Set([
'api.example.com',
'cdn.example.com',
]);
const target = new URL(req.body.url);
if (!ALLOWED_HOSTS.has(target.hostname)) {
return res.status(400).send('host not allowed');
}
const r = await fetch(target);
Option 2 — route through a hardened proxy that strips inbound URLs and only fetches from a known set of upstreams.
Option 3 — eliminate the feature. Most "fetch arbitrary
URL on behalf of the user" features can be replaced with a
client-side <iframe> or a server-side fetch from a
catalog rather than a free-form URL.
How to suppress this finding
Only if the URL is genuinely safe — for example, a private API where the caller is already authenticated and you've sanitised the host against an allow-list inside a helper the rule can't see across.
// rastray-ignore: RSTR-SSRF-001
const r = await safeFetch(req.body.url);
Or project-wide in .rastray.toml:
[rules]
"RSTR-SSRF-001" = { severity = "low" } # downgrade to informational