GHSA-j4rj-2jr5-m439 ssrfcheck Vulnerable to Server-Side Request Forgery (SSRF) and Incomplete List of Disallowed Inputs
Summary`ssrfcheck` v1.3.0 (latest) fails to block Server-Side Request Forgery attacks when the target private IP address is encoded as an IPv4-mapped IPv6 address (e.g. `http://[::ffff:127.0.0.1]/`). The WHATWG URL parser built into Node.js silently normalizes the IPv4 notation inside the brackets to compressed hex form (`[::ffff:7f00:1]`) before the library's private-IP regex ever runs. The regex was written to match dot-notation only and therefore never matches any real input — all seven IANA private IPv4 ranges, including the AWS/GCP/Azure metadata address `169.254.169.254`, are bypassed. Any application using `isSSRFSafeURL()` to guard HTTP requests made with user-supplied URLs is fully exposed to SSRF.Details**Vulnerable file:** `src/is-private-ip.js`The library detects IPv6 private addresses using the `privIp6()` function. The relevant portion:The third line is the IPv4-mapped IPv6 check. It expects input in the form `::ffff:127.0.0.1` (dots). However, the IP is extracted from the URL using `url.hostname`, which goes through the WHATWG URL parser first.**How WHATWG URL normalizes the address** (`src/parse-url.js`):The WHATWG URL spec (§5.3 IPv6 serializer) converts all embedded IPv4 notation to two 16-bit hex groups during parsing:So by the time the regex `/^::f{4}:(\d+)\.(\d+)\.(\d+)\.(\d+)$/` runs, the string it receives is `::ffff:7f00:1` — no dots, no match. The regex has been dead code since Node.js adopted WHATWG URL (v10+).**Entry point** (`src/index.js`):PoC**Environment:** Node.js >= 10, ssrfcheck any version including v1.3.0 (latest). No configuration required — default options are vulnerable.**Setup:****Step 1 — confirm WHATWG URL normalization:**Expected output — confirms WHATWG drops dots:**Step 2 — trigger the bypass:****Confirmed output (live-verified on Node.js v20.20.2, ssrfcheck v1.3.0, Zorin OS Linux, 2026-04-12):**7/7 private-range variants bypass the check. Baseline dot-notation detections remain intact, confirming the bug is specific to the WHATWG normalization path.**Full automated verification script (`verify-ssrfcheck.js`):**Run:VIDEO POC ASCII CAST[![asciicast](https://asciinema.org/a/CxTKMwrlcHUUbQT8.svg)](https://asciinema.org/a/CxTKMwrlcHUUbQT8)--Impact**Vulnerability type:** Server-Side Request Forgery (SSRF) — complete protection bypass**Who is impacted:** Any Node.js application that:Accepts a URL from an untrusted source (user input, API parameter, webhook payload)Uses `isSSRFSafeURL()` from `ssrfcheck` to validate that URL before making an outbound HTTP requestRuns on Node.js >= 10 (WHATWG URL parser enabled — all supported versions as of 2026)**Concrete impact scenarios:****Cloud metadata theft:** On AWS, GCP, or Azure, attacker sends `http://[::ffff:169.254.169.254]/latest/metadat**Internal network pivoting:** Attacker reaches services on `10.x.x.x`, `172.16.x.x`, `192.168.x.x` that are not exposed to the internet, bypassing the only protection layer.**Localhost access:** Attacker reaches `http://[::ffff:127.0.0.1]/admin` or any service bound to loopback on the server.The bypass requires no authentication, no special privileges, and no non-default configuration. It works against every version of ssrfcheck on every Node.js version >= 10.Weaknesses**CWE-918** — Server-Side Request Forgery (SSRF)
**CWE-184** — Incomplete List of Disallowed InputsSuggested FixReplace the hand-rolled regex denylist in `src/is-private-ip.js` with Node's built-in `net.BlockList`, which operates on parsed IP values and is immune to string representation differences:The `::ffff:0:0/96` subnet entry covers the entire IPv4-mapped IPv6 space in a single rule. `BlockList.check()` parses the IP numerically, so it is unaffected by WHATWG URL normalization or any other string representation.
CVSS v3.1 score: 8.2