Your custom lock was installed but the deadbolt was quietly left out during assembly
CVE-2026-84961 affects undici's BalancedPool class in versions 7.24.1 through 7.29.0 and 8.0.0 through 8.10.1. The constructor passes its options object through an internal deep-clone that uses JSON serialization. Because JSON.stringify cannot represent functions, any caller-supplied connect or tls option containing a checkServerIdentity callback, a custom ca certificate chain expressed via a function reference, or similar non-serializable TLS settings is silently dropped before reaching the TLS layer. The connection then falls back to Node.js's default Mozilla CA trust store, accepting certificates that the application explicitly intended to reject.
The vendor rated this HIGH (7.4) with AC:H, which is honest about the MITM prerequisite but overstates the real-world blast radius. The bug only fires when three conditions align simultaneously: the app uses BalancedPool specifically (not Pool, Client, or Agent), it passes function-valued TLS options, *and* an attacker holds a man-in-the-middle position. The intersection of those three conditions in production is narrow. Most undici consumers use fetch(), request(), or plain Pool — none of which route through the broken clone path. The vendor severity is fair for the theoretical ceiling but oversells the practical risk for typical Node.js deployments.
4 steps from start to impact.
Target uses BalancedPool with custom TLS validation
BalancedPool and pass a connect or tls option containing function-valued properties such as checkServerIdentity or a dynamic ca loader. This is the only class affected — Pool, Client, Agent, and the global fetch() API are not vulnerable. This pattern is most common in microservices doing certificate pinning or connecting to endpoints secured by an internal CA.- Target application uses undici BalancedPool
- Application passes function-valued TLS options (checkServerIdentity, ca callback)
- Affected undici version (7.24.1–7.29.0 or 8.0.0–8.10.1)
- BalancedPool is a specialized class; most undici usage goes through fetch(), request(), or Pool
- Custom TLS validation callbacks are a niche pattern, primarily used in zero-trust or cert-pinning architectures
- Default undici usage inherits Node.js's standard TLS behavior, which is unaffected
Attacker achieves man-in-the-middle position
- Network-level access between application and upstream origin
- Ability to intercept and modify TCP/TLS handshake
- MITM on production server-to-server traffic is substantially harder than client-to-server
- Cloud provider backbone and VPC isolation reduce the viable attack surface
- Network detection tools (IDS/IPS, anomaly detection) may flag ARP spoofing or DNS hijack attempts
Present valid-looking certificate from public CA
checkServerIdentity callback silently dropped, the TLS stack falls back to Node.js's default trust store (Mozilla CA bundle). The attacker presents a certificate for the target hostname signed by any publicly trusted CA. Since the application intended to pin to a specific CA or validate additional properties, this certificate would normally be rejected — but the dropped callback means it is accepted.- Attacker obtains a certificate for the target hostname from any public CA
- Domain validation or other CA issuance succeeds
- Obtaining a legitimate certificate for an internal hostname is non-trivial
- Many internal services use non-routable hostnames that public CAs will not issue certificates for
- Certificate Transparency logs would record the issuance, enabling detection
Intercept or modify TLS-protected traffic
BalancedPool connection — other connections in the same application are unaffected.- Successful MITM with accepted certificate
- Target connection carries sensitive data
- Blast radius is limited to the BalancedPool connections, not all application traffic
- Application-layer authentication (mutual TLS client certs, signed requests) may still detect tampering
- Response integrity checks or HMAC-signed payloads would fail
BalancedPool. Replacing it with Pool (single origin) or Agent (automatic connection management) preserves your custom TLS callbacks. This is the fastest mitigation — deploy within the noisgate MEDIUM remediation window of 365 days, though earlier is better if you have confirmed BalancedPool + custom TLS in your codebase.new BalancedPool in your Node.js projects and check whether the options object contains checkServerIdentity, ca as a function, or other non-serializable TLS properties. If none are found, this CVE does not affect you.npm audit fix or explicitly update undici. Verify with npm ls undici that no transitive dependency pulls in a vulnerable version.- WAF / API Gateway in front of the application — This vulnerability is in the application's *outbound* connections to upstream origins, not in inbound request handling. A WAF inspects inbound traffic and cannot see or control the application's outbound TLS handshakes.
- Network-level TLS inspection (SSL proxy) — A TLS-inspecting proxy between the app and its upstreams would replace the TLS session entirely, but it would also break the custom certificate validation the app was trying to do in the first place. It masks the bug without fixing it.
- Upgrading Node.js alone — While Node.js bundles undici, many applications install undici as a direct dependency. Upgrading Node.js may not update the application's pinned undici version. Check
npm ls undicito confirm which version your application actually loads.
The supporting signals.
| In-the-wild exploitation | Not observed. No reports from CISA, GreyNoise, or vendor advisories indicate active exploitation. Not listed on CISA KEV. |
|---|---|
| Proof of concept | No public PoC. The advisory describes the technical mechanism (JSON serialization drops functions) but no weaponized exploit or reproduction script has been published. Reported by MegaManSec. |
| EPSS score | Not yet scored — CVE disclosed 2026-09-04; EPSS typically populates within 7-14 days. Expected to be low given the MITM + niche usage prerequisites. |
| KEV status | Not listed on CISA Known Exploited Vulnerabilities catalog as of 2026-09-05. |
| CVSS vector | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N — Network-accessible but high attack complexity (MITM required). No privileges or user interaction needed. High confidentiality and integrity impact, no availability impact. Scope unchanged. |
| Affected versions | undici 7.24.1–7.29.0 and 8.0.0–8.10.1. Earlier major versions (5.x, 6.x) do not contain the vulnerable deep-clone path in BalancedPool. |
| Fixed versions | undici 7.29.1 and 8.10.2. Node.js core bundles undici — check process.versions.undici to see if your Node.js build ships a patched version. |
| Exposure footprint | undici has ~150M+ weekly npm downloads. However, BalancedPool usage with custom TLS callbacks represents a small fraction. No Shodan/Censys exposure data applies (library-level bug, not a listening service). |
| Disclosure date | 2026-09-04 via GHSA-w293-vg96-wgc3. |
| Reporter / credits | Reported by MegaManSec; patched by mcollina, reviewed by UlisesGascon. |
Sources.
Why this verdict
- Narrow affected surface: Only
BalancedPoolwith function-valuedconnect/tlsoptions is affected. The dominant undici usage patterns (fetch(),request(),Pool,Client,Agent) are unaffected. This shrinks the vulnerable population to a small percentage of the 150M+ weekly install base. - MITM prerequisite (AC:H): Exploiting this requires an active man-in-the-middle position on production server-to-server traffic. In cloud-native deployments (the dominant undici environment), this means compromising VPC networking or provider backbone — a high bar that implies the attacker already has significant access.
- No exploitation evidence: No in-the-wild exploitation, no public PoC, no KEV listing. The vulnerability was disclosed responsibly with patches available same-day. The theoretical attack is well-understood but has not been demonstrated.
- Role multiplier: undici's BalancedPool *could* be used in high-value roles (API gateways, service mesh proxies, CI/CD pipeline HTTP clients). In these roles, a successful MITM could expose secrets or enable supply-chain injection. However, (a) BalancedPool with custom TLS pinning is an uncommon pattern even in these roles, and (b) the MITM prerequisite means the attacker is already inside the network. The worst plausible high-value outcome is per-connection data interception, not fleet compromise or domain takeover — blast radius is connection-scoped, not host- or domain-scoped. This does not trigger the HIGH floor.
- Silent failure mode is concerning: The callback is dropped *silently* with no error or warning logged, which means affected applications have no indication their TLS validation is ineffective. This is the primary reason the verdict remains MEDIUM rather than LOW.
Why not higher?
The vendor's HIGH rating assumes the attacker can achieve MITM and the target uses the exact affected code path. In practice, the intersection of BalancedPool usage, custom TLS callbacks, and attainable MITM position is narrow. The blast radius is limited to specific connections within one application — there is no lateral movement, privilege escalation, or fleet-scale impact from this vulnerability alone. No high-value role deployment pattern triggers the HIGH floor because the outcome is connection-scoped interception, not domain or fleet compromise.
Why not lower?
The silent failure mode is genuinely dangerous for the applications that *do* use this pattern. Certificate pinning and custom CA validation are security controls deployed specifically in high-sensitivity contexts (financial APIs, healthcare data exchanges, zero-trust microservices). Silently removing those controls without any error undermines the application's security assumptions in exactly the scenarios where TLS validation matters most. The absence of any runtime warning makes detection difficult without explicit version auditing.
Crowdsourced verification payload.
Run this on any host where your Node.js application is deployed or in your CI pipeline. Execute with bash check_cve_2026_84961.sh /path/to/your/project. No special privileges required — it reads node_modules and package-lock.json.
#!/usr/bin/env bash
# check_cve_2026_84961.sh — Detect undici versions vulnerable to CVE-2026-84961
# Usage: bash check_cve_2026_84961.sh [/path/to/project]
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -euo pipefail
PROJECT_DIR="${1:-.}"
VULNERABLE=0
if [ ! -d "$PROJECT_DIR/node_modules" ]; then
echo "UNKNOWN — no node_modules found in $PROJECT_DIR"
exit 2
fi
# Find all installed undici versions
find "$PROJECT_DIR/node_modules" -path '*/undici/package.json' -not -path '*/node_modules/.cache/*' | while read -r pkg; do
version=$(grep -o '"version": *"[^"]*"' "$pkg" | head -1 | grep -o '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*')
if [ -z "$version" ]; then
continue
fi
major=$(echo "$version" | cut -d. -f1)
minor=$(echo "$version" | cut -d. -f2)
patch=$(echo "$version" | cut -d. -f3)
vuln="no"
# Check 7.24.1 through 7.29.0
if [ "$major" -eq 7 ]; then
if [ "$minor" -gt 24 ] && [ "$minor" -lt 29 ]; then
vuln="yes"
elif [ "$minor" -eq 24 ] && [ "$patch" -ge 1 ]; then
vuln="yes"
elif [ "$minor" -eq 29 ] && [ "$patch" -eq 0 ]; then
vuln="yes"
fi
fi
# Check 8.0.0 through 8.10.1
if [ "$major" -eq 8 ]; then
if [ "$minor" -lt 10 ]; then
vuln="yes"
elif [ "$minor" -eq 10 ] && [ "$patch" -le 1 ]; then
vuln="yes"
fi
fi
if [ "$vuln" = "yes" ]; then
echo "VULNERABLE — undici $version found at $pkg"
exit 1
else
echo "PATCHED — undici $version found at $pkg"
fi
done
# If the loop didn't exit, check if undici was found at all
if ! find "$PROJECT_DIR/node_modules" -path '*/undici/package.json' -not -path '*/node_modules/.cache/*' | grep -q .; then
echo "UNKNOWN — undici not found in $PROJECT_DIR"
exit 2
fi
exit 0