Someone left one sink unsanitized in an otherwise well-plumbed kitchen, but you need a very specific wrench to reach it
CVE-2026-15157 is a CRLF injection flaw in undici, the HTTP/1.1 client bundled into Node.js and downloaded ~139 million times per week on npm. When an application uses undici's low-level dispatcher APIs (request(), stream(), pipeline(), dispatch()) and passes a hand-rolled "duck-typed" blob-like body whose .type property is derived from untrusted input, an attacker can inject \r\n sequences into the outgoing Content-Type header. This lets them append arbitrary HTTP headers and, in the worst case, smuggle a second HTTP request to the upstream server. The flaw exists because body.type is pushed directly into the wire format without passing through isValidHeaderValue() — every other header sink in undici already has this check. Native Blob objects are safe (they strip CRLF in the constructor), and fetch() is unaffected because it validates all headers independently. Affected versions: undici < 6.28.0, 7.0.0 – 7.28.x, and 8.0.0 – 8.8.x. Fixed in 6.28.0, 7.29.0, and 8.9.0.
The vendor rated this MEDIUM / 4.2 and that is directionally correct. The CVSS vector (AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N) already encodes most of the friction: high attack complexity, user interaction required, unchanged scope, and low confidentiality/integrity impact with no availability impact. In practice, real-world exploitability is *even narrower* than what the vector suggests. The attacker doesn't target undici — they target an *application* that happens to build duck-typed blob bodies from user input and feed them to undici's non-fetch APIs. That coding pattern is uncommon. Most Node.js applications use fetch() (safe), native Blob (safe), or simply never construct blob-like objects from user-controlled MIME types. The 4.2 is fair; if anything, the true risk for most enterprises sits a fraction lower.
4 steps from start to impact.
Identify a target application using undici dispatcher APIs
undici.request(), undici.stream(), undici.pipeline(), or undici.dispatch() — not fetch(). These are lower-level APIs used in custom HTTP clients, proxy layers, and orchestration tooling. The attacker needs to identify that the application accepts user-supplied data that flows into the body of one of these calls.- Target application uses undici's non-fetch dispatcher APIs
- Application is reachable by attacker (directly or via user interaction)
- Most Node.js applications default to
fetch()or useaxios/got, never touching undici's low-level API directly - No banner or fingerprint reveals which HTTP client library a backend uses
body.type usage in undici dispatcher calls.Find an untrusted input path into a duck-typed blob's .type property
{ type, arrayBuffer() } or similar blob-like shape, where the .type value comes from attacker-controlled input (e.g., a user-supplied MIME type from an upload form, API parameter, or webhook payload). Native Blob objects strip CRLF in their constructor, so only hand-rolled duck-typed blobs are exploitable.- Application constructs duck-typed blob-like bodies (not native Blob)
- The .type property is derived from untrusted user input
- Duck-typed blob bodies are a niche pattern — most developers use native Blob, Buffer, or string bodies
- Even custom blob-like objects rarely derive .type from raw user input; most hardcode a MIME string
Inject CRLF sequences into the .type value
text/plain\r\nX-Injected: malicious\r\n\r\nGET /admin HTTP/1.1\r\nHost: internal as the MIME type. Because undici's client-h1.js writes body.type directly into the Content-Type header without calling isValidHeaderValue(), the CRLF bytes pass through to the TCP socket verbatim.- Application does not independently sanitize the .type value
- undici version is < 6.28.0, 7.0.0–7.28.x, or 8.0.0–8.8.x
- Many frameworks and middleware strip or reject control characters in user input before it reaches application logic
- WAFs with HTTP smuggling rule sets may detect anomalous Content-Type values
\r\n in Content-Type headers can catch this. No specific IDS/IPS signature for CVE-2026-15157 exists yet.Achieve header injection or HTTP request smuggling
- Upstream server interprets the smuggled headers/request
- No TLS-terminating proxy normalizes the outgoing request before it reaches the upstream
- Modern reverse proxies (nginx, envoy, HAProxy) often normalize or reject malformed HTTP requests
- HTTP/2 upstreams are immune — smuggling only works on HTTP/1.1 connections
- Impact is confined to the single upstream connection, not lateral movement
The supporting signals.
| In-the-Wild Exploitation | None confirmed. No campaigns, no CISA KEV listing, no vendor acknowledgment of active exploitation. Disclosed 2026-07-29 (yesterday). |
|---|---|
| Proof-of-Concept | No public PoC. The advisory by maintainer mcollina (Matteo Collina) describes the vulnerable code path in lib/dispatcher/client-h1.js but does not include a working exploit. The same defect class was demonstrated in CVE-2022-35948 — that prior PoC is conceptually transferable but targets a different sink. |
| EPSS Score | Not yet scored. CVE was published 2026-07-29; FIRST EPSS typically populates within 7–14 days. For the similar CVE-2022-35948 the EPSS settled around the 5th–10th percentile — expect a similar range here given the high AC and UI:R. |
| KEV Status | Not listed. No CISA Known Exploited Vulnerabilities catalog entry as of 2026-07-30. |
| CVSS Vector | CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N — Network-accessible but high complexity, requires user interaction, no scope change, low C+I, no availability impact. This is an honest vector. |
| Affected Versions | undici < 6.28.0, 7.0.0 – 7.28.x, 8.0.0 – 8.8.x. Node.js 24 bundles undici 7.x — check process.versions.undici at runtime. Node.js 22 LTS bundles undici 6.x. |
| Fixed Versions | 6.28.0, 7.29.0, 8.9.0. Expect Node.js 22.x and 24.x point releases to bundle the fix shortly; watch nodejs/node releases. |
| Scanning / Exposure | undici is a client-side library — it has no listening port and is invisible to Shodan/Censys/GreyNoise. Exposure depends entirely on application code patterns. npm reports ~139M weekly downloads across ~6,865 dependent packages, but the vulnerable code path (duck-typed blob + non-fetch API + untrusted .type) is a small fraction of that surface. |
| Disclosure Timeline | CVE reserved 2026-07-08; GHSA GHSA-m8rv-5g2x-5cg5 and fix published 2026-07-29. |
| Reporter | mcollina (Matteo Collina, undici core maintainer). Remediation reviewed by UlisesGascon. This was found internally by the project, not by an external attacker or bug-bounty researcher. |
noisgate verdict.
The single most decisive factor is the narrow application-level code pattern required: the attacker must find a Node.js app that constructs duck-typed blob bodies from untrusted input and feeds them to undici's non-fetch dispatcher APIs — a coding pattern rare enough that the vast majority of undici's 139M-download installed base is unexposed. The vendor's MEDIUM is directionally correct; the real-world friction compresses the score slightly below 4.2.
Why this verdict
- Attack-complexity friction: The CVSS vector already reflects AC:H and UI:R, but the real-world narrowing is even steeper — the attacker must chain three application-level prerequisites (non-fetch API, duck-typed blob, untrusted .type) that have no external fingerprint and no reliable way to enumerate remotely.
- Safe-by-default paths dominate: Native
Blobobjects (which strip CRLF in the constructor) andfetch()(which validates all headers) are the overwhelmingly common usage patterns for undici. The vulnerable path throughrequest()/stream()/pipeline()with a hand-rolled blob-like body is a niche power-user pattern. - No exploitation evidence or PoC: Disclosed yesterday by the project maintainer, no public exploit code, no campaigns, no KEV listing. The prior same-class CVE-2022-35948 never saw meaningful in-the-wild exploitation either.
- Role multiplier: undici is a general-purpose HTTP *client* library, not infrastructure-role software. (a) *Low-value role:* dev tooling, CLI scripts — chain succeeds if code pattern exists, blast radius is a single outgoing HTTP connection, local impact only. (b) *Typical role:* backend microservice making upstream API calls — chain succeeds if pattern exists, blast radius is request smuggling against one upstream, limited to that connection. (c) *High-value role:* Node.js-based API gateway or proxy layer — chain succeeds if the gateway constructs duck-typed blob bodies from user-supplied Content-Type (uncommon; most gateways pass headers through or use native Blob). Blast radius could be request smuggling against backend services, but scope remains unchanged (S:U) and impact is C:L/I:L. This does NOT reach domain/fleet/supply-chain scale. Conclusion: No high-value role produces a blast radius that triggers the HIGH floor — the worst plausible outcome is header injection against a single upstream connection.
- Installed-base share in high-value roles: While undici has massive download counts, the *component itself* is not canonically a high-value-role component (it is not a hypervisor, IdP, PAM, backup system, kernel agent, or network edge appliance). The ≥10% canonical threshold for CRITICAL floor does not apply. The ≥1% threshold for HIGH floor does not apply because even in gateway roles the blast radius does not reach fleet/domain/supply-chain scale.
Why not higher?
Upgrading to HIGH would require either active exploitation, a public PoC lowering the bar for attackers, or a plausible attack path that reaches fleet-scale or identity-scale compromise. None of these conditions exist. The vulnerability requires an uncommon application-level coding pattern, has no scope change, and the worst-case outcome is header injection on a single outgoing HTTP/1.1 connection — not lateral movement, not privilege escalation, not code execution.
Why not lower?
Dropping to LOW would undercount the *potential* severity of HTTP request smuggling when it does land — cache poisoning and auth bypass against internal APIs are real outcomes even if the path is narrow. The 139M weekly download base means even a small percentage of vulnerable applications is a non-trivial absolute number. The bug class (CRLF injection) is well-understood and, once a PoC drops, exploitation is trivial for the specific applications that match the pattern.
What to do — in priority order.
- Pin an explicit Content-Type header in request options — Setting
headers: { 'content-type': 'application/octet-stream' }(or any validated value) on the undici request options object causes undici to skip readingbody.typeentirely. This is a one-line code change per call site and fully neutralizes the vulnerability. No mitigation SLA applies for MEDIUM — go straight to the 365-day remediation window. - Replace duck-typed blobs with native Blob objects — Native
Blobconstructors strip\r\nfrom the type parameter. Swapping{ type: userInput, arrayBuffer() { ... } }tonew Blob([data], { type: userInput })eliminates the injection vector at the source. Audit your codebase withgrep -rn 'arrayBuffer\|type.*blob' --include='*.js' --include='*.ts'. - Use fetch() instead of undici dispatcher APIs — The
fetch()path validates all headers independently and is not affected. If your application does not need the low-level streaming/pipeline features, switching tofetch()removes the vulnerable code path entirely. - Upgrade undici to 6.28.0 / 7.29.0 / 8.9.0 — The definitive fix. For Node.js 22 LTS and 24 Current, monitor for a point release that bundles the patched undici version — check
process.versions.undiciafter upgrading Node.js.
- WAF rules alone — undici is a client-side library making *outgoing* requests; your WAF inspects *incoming* requests to your server. The CRLF injection occurs on the wire between your app and its upstream, which is typically not WAF-inspected.
- Upgrading Node.js without checking the bundled undici version — Node.js point releases don't always bump undici. Verify with
node -e 'console.log(process.versions.undici)'after any Node.js update. - Input validation at the API gateway layer — The injection happens in application code constructing the blob body, not in HTTP request headers arriving at your gateway. Gateway-level header sanitization doesn't reach this code path.
Crowdsourced verification payload.
Run this on any host with node and npm installed to check whether the installed undici version is vulnerable. Works for both standalone npm install undici and the version bundled in Node.js. No special privileges needed. Example: bash check_cve_2026_15157.sh
#!/usr/bin/env bash
# check_cve_2026_15157.sh
# Checks whether the installed undici version is vulnerable to CVE-2026-15157
# (CRLF injection via duck-typed blob .type property)
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
VULNERABLE=false
CHECKED=false
# Helper: compare semver "$1 < $2" (major.minor.patch)
ver_lt() {
local IFS='.'
read -ra A <<< "$1"
read -ra B <<< "$2"
for i in 0 1 2; do
local a=${A[$i]:-0}
local b=${B[$i]:-0}
if (( a < b )); then return 0; fi
if (( a > b )); then return 1; fi
done
return 1 # equal
}
# 1. Check Node.js bundled undici
if command -v node &>/dev/null; then
BUNDLED=$(node -e 'try { console.log(process.versions.undici || "none") } catch(e) { console.log("none") }' 2>/dev/null || echo "none")
if [[ "$BUNDLED" != "none" ]]; then
CHECKED=true
MAJOR=$(echo "$BUNDLED" | cut -d. -f1)
if [[ "$MAJOR" -le 6 ]]; then
if ver_lt "$BUNDLED" "6.28.0"; then
echo "[!] Node.js bundled undici $BUNDLED is VULNERABLE (need >= 6.28.0)"
VULNERABLE=true
else
echo "[+] Node.js bundled undici $BUNDLED is PATCHED"
fi
elif [[ "$MAJOR" -eq 7 ]]; then
if ver_lt "$BUNDLED" "7.29.0"; then
echo "[!] Node.js bundled undici $BUNDLED is VULNERABLE (need >= 7.29.0)"
VULNERABLE=true
else
echo "[+] Node.js bundled undici $BUNDLED is PATCHED"
fi
elif [[ "$MAJOR" -ge 8 ]]; then
if ver_lt "$BUNDLED" "8.9.0"; then
echo "[!] Node.js bundled undici $BUNDLED is VULNERABLE (need >= 8.9.0)"
VULNERABLE=true
else
echo "[+] Node.js bundled undici $BUNDLED is PATCHED"
fi
fi
fi
fi
# 2. Check npm-installed undici in current project and global
for DIR in "$(pwd)/node_modules/undici" "$(npm root -g 2>/dev/null)/undici"; do
PKG="$DIR/package.json"
if [[ -f "$PKG" ]]; then
CHECKED=true
VER=$(node -e "console.log(require('$PKG').version)" 2>/dev/null || echo "unknown")
if [[ "$VER" == "unknown" ]]; then continue; fi
MAJOR=$(echo "$VER" | cut -d. -f1)
LABEL="npm undici $VER ($DIR)"
if [[ "$MAJOR" -le 6 ]]; then
if ver_lt "$VER" "6.28.0"; then
echo "[!] $LABEL is VULNERABLE"
VULNERABLE=true
else
echo "[+] $LABEL is PATCHED"
fi
elif [[ "$MAJOR" -eq 7 ]]; then
if ver_lt "$VER" "7.29.0"; then
echo "[!] $LABEL is VULNERABLE"
VULNERABLE=true
else
echo "[+] $LABEL is PATCHED"
fi
elif [[ "$MAJOR" -ge 8 ]]; then
if ver_lt "$VER" "8.9.0"; then
echo "[!] $LABEL is VULNERABLE"
VULNERABLE=true
else
echo "[+] $LABEL is PATCHED"
fi
fi
fi
done
# 3. Verdict
if [[ "$CHECKED" == false ]]; then
echo "UNKNOWN — no undici installation found"
exit 2
elif [[ "$VULNERABLE" == true ]]; then
echo "VULNERABLE"
exit 1
else
echo "PATCHED"
exit 0
fiIf you remember one thing.
content-type header), so there's no reason to wait a full year. Practically: add the undici bump to your next scheduled Node.js dependency update cycle. If you run Node.js 22 LTS or 24 Current, watch for the next point release that bundles the patched undici — verify with node -e 'console.log(process.versions.undici)'. In the meantime, audit your codebase for the specific vulnerable pattern: grep for duck-typed blob-like bodies whose .type comes from user input in undici.request() / stream() / pipeline() / dispatch() calls. If you find any, apply the one-line compensating control (set an explicit content-type header) today. If you use only fetch() or native Blob objects, you are already safe and this CVE is informational for your environment.Sources
- GHSA-m8rv-5g2x-5cg5 — GitHub Security Advisory
- CVE-2026-15157 — THREATINT
- Vigilance.fr — Node.js undici header injection via Content-Type
- SecurityOnline — Four undici Vulnerabilities Affect 133M Weekly Downloads
- Tenable — Node.js Module Undici Multiple Vulnerabilities
- undici npm package
- Node.js 24.0.0 Release Notes (bundled undici)
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.