VNX-437: Incomplete Model of Endpoint Features
Reports JavaScript/TypeScript and Python code that vets a URL with a simpler model than the client uses to act on it (a raw-string startsWith/includes/endsWith/in check against a host, or Node's legacy url.parse()) and then requests or redirects to the unchanged string.
| Field | Value |
|---|---|
| Rule ID | VNX-437 |
| Severity | Low |
| SARIF level | note |
| Kind | sast |
| Languages | JavaScript / TypeScript, Python |
| CWE | CWE-437 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:L/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:L/DI:H/EX:L/EC:N/P:L |
| Tags | incomplete-model-endpoint-features url-parsing ssrf open-redirect cwe-437 |
| Source | internal/sast/rules/vnx-437.rego |
Overview
VNX-437 maps to CWE-437: Incomplete Model of Endpoint Features. The check and the HTTP client or browser disagree about which host a URL names. A string check does not know about userinfo, subdomains or query strings, so https://trusted.example.com.evil.net, https://trusted.example.com@evil.net and https://evil.net/?trusted.example.com all pass it, and the request goes to the attacker (SSRF or open redirect).
What Gets Flagged
The line that requests or redirects, when within fifteen lines above it the same variable was checked as a raw string against a host literal:
startsWith/startswithwith a scheme and host but no/after the host;endsWith/endswith,includes,indexOfor Pythoninwith a host literal;- a regular-expression literal whose pattern ends at the host, tested with
.test()(/^https:\/\/cdn\.acme\.com/.test(u)).
Also, in Node, a request of the original string when the host, hostname, protocol or origin was vetted on the result of the legacy url.parse(), whether assigned, read inline (url.parse(u).hostname) or destructured (const { hostname } = url.parse(u)).
if (!target.startsWith('https://cdn.acme-media.com')) return res.sendStatus(400);
const upstream = await fetch(target); // FLAGGED
const parsed = url.parse(hookUrl);
if (!ALLOWED.has(parsed.hostname)) throw new Error('denied');
https.request(hookUrl, { method: 'POST' }); // FLAGGED
if not url.startswith("https://api.partner-data.com/"): # ends at "/": not flagged
abort(400)
return requests.get(url, timeout=5)
Not reported: prefix checks that end at a path separator, checks on a parsed new URL()/urlparse hostname (including a re-parse between the raw check and the request), checks not followed by a request of the same variable, a positive check whose block does not contain the request, scheme-only checks, comments and strings. Request smuggling through a lenient HTTP parser is VNX-444.
Remediation
- Parse the URL once (
new URL(),urllib.parse.urlsplit) and compare the hostname exactly against an allow-list. - Request the parsed URL, not the original string.
- If a prefix check is unavoidable, end it with
/after the host.
References
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-437
// nosec vnx-437
Or record an ignore rule: vulnetix ignore add --rule vnx-437 --reason "...". See Suppressing findings.