Like a courier who staples the old shipping label onto a repackaged box, so downstream reads the wrong weight
CVE-2026-16728 is a response desynchronization flaw in undici's retry interceptor (interceptors.retry()). When an upstream server returns a partial (206) response with a Content-Length that doesn't match the bytes actually delivered and then drops the connection, the retry interceptor reassembles the body from the resumed transfer — but leaves the original, now-stale Content-Length header in place. Any application that forwards that response downstream (proxies, API gateways, BFF layers) emits an HTTP message whose framing metadata contradicts the actual payload. 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, which also ship inside Node.js 22.23.2, 24.18.1, and 26.5.1 respectively.
The vendor's MEDIUM / 4.8 rating is honest and, unusually, already accounts for the real-world friction. The CVSS vector specifies AC:H (high attack complexity) — the attacker must control or compromise an upstream origin, the victim application must have explicitly enabled the retry interceptor (it is *not* on by default), and the application must forward raw headers without recalculating Content-Length. That triple gate narrows the exploitable population to a small slice of Node.js proxy/gateway deployments. Impact is capped at C:L / I:L / A:N: response confusion, possible cache poisoning, and connection hangs — not code execution, not credential theft, not lateral movement. There is no reason to override the vendor here.
4 steps from start to impact.
Attacker controls or compromises an upstream origin
Content-Length + early socket close cannot be injected.- Attacker controls or has compromised an HTTP origin the undici client fetches from
- In most enterprise deployments, upstream origins are internal microservices or trusted third-party APIs — not attacker-controlled endpoints
- If the upstream is behind a service mesh or mTLS, compromise is an additional step
Undici retry interceptor is enabled on the client
interceptors.retry() via the undici Dispatcher API. Node.js's built-in fetch() does not enable the retry interceptor by default; the developer must wire it up manually. This is a hard prerequisite — without it, the partial-response-reassembly logic never fires.- Application code explicitly configures
interceptors.retry()on the Dispatcher
- Retry interceptor is opt-in, not default — the vast majority of undici /
fetch()consumers never enable it - Even when retry is desired, many teams use higher-level retry wrappers (axios-retry, got retry, etc.) rather than undici's native interceptor
interceptors.retry in the codebaseUpstream returns partial response with stale Content-Length and drops connection
Content-Length: 300 header but delivers only 99 bytes before closing the socket. The retry interceptor detects the incomplete transfer and retries, fetching the remaining bytes from the upstream's resumed offset — but preserves the original Content-Length: 300 header on the reassembled response.- Upstream cooperates with the retry by serving resumable partial content
- Socket close happens at a point that triggers undici's retry logic rather than a hard error
- Timing and byte-level control over the partial response are non-trivial
- Many upstreams don't support Range/resume, causing the retry to restart from scratch (which may not trigger the bug)
Application forwards response with stale Content-Length downstream
Content-Length. The downstream client or intermediary now sees a mismatch between the declared and actual body length, causing response desynchronization — connection hangs, truncated responses, or (in a persistent-connection scenario) reading the next response's bytes as part of the current one.- Application is a forwarding proxy / API gateway that pipes headers verbatim
- Downstream connection is persistent (HTTP/1.1 keep-alive) so desync actually matters
- Many Node.js proxy frameworks (http-proxy, fastify-reply-from) recalculate Content-Length or use Transfer-Encoding: chunked
- HTTP/2 downstream connections use their own framing and are not affected by Content-Length desync
The supporting signals.
| In-the-wild exploitation | None observed. No campaigns, no CISA KEV listing, no vendor acknowledgment of active exploitation. Disclosed 2026-07-29 — less than 24 hours old at time of assessment. |
|---|---|
| Proof-of-concept | No public PoC. The advisory credits samuel871211 as the reporter. No exploit code in GitHub, ExploitDB, or nuclei-templates. Reproduction requires a custom upstream server + retry-interceptor-enabled client + forwarding proxy — non-trivial to weaponize. |
| EPSS | Not yet scored — CVE was published 2026-07-29; FIRST EPSS model has not ingested it. Expected to land in the low single-digit percentile given AC:H and no PoC. |
| KEV status | Not listed. No CISA KEV entry as of 2026-07-30. |
| CVSS vector | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N — Network-reachable but high complexity, no privileges or user interaction needed, unchanged scope, low confidentiality and integrity impact, no availability impact. The AC:H is the dominant friction factor. |
| Affected versions | undici < 6.28.0 · 7.0.0 – 7.28.x · 8.0.0 – 8.8.x. Also affects Node.js bundled undici in Node 22.x < 22.23.2, 24.x < 24.18.1, 26.x < 26.5.1. |
| Fixed versions | undici 6.28.0, 7.29.0, 8.9.0. Node.js 22.23.2, 24.18.1, 26.5.1 (all released 2026-07-29). |
| Scanning / exposure data | Undici is not directly fingerprinted by Shodan/Censys — it is a client-side library, not a listening service. ~113–139M weekly npm downloads, but the retry interceptor is opt-in. No reliable way to measure externally exposed population. |
| Disclosure date | 2026-07-29 via GitHub Security Advisory GHSA-8xcm-r25x-g524. |
| Reporter / credits | samuel871211 (reporter), mcollina (Matteo Collina, remediation developer), UlisesGascon (remediation reviewer). |
noisgate verdict.
The single most decisive factor is that the retry interceptor is opt-in, not default — the vast majority of the ~139M weekly undici consumers never enable it, shrinking the exploitable population to a narrow slice of custom proxy/gateway builds. Even when triggered, the ceiling is response-level desynchronization (C:L/I:L), not code execution or credential theft.
Why this verdict
- Opt-in feature gate:
interceptors.retry()must be explicitly wired up by the developer. Node.js's built-infetch()does not enable it. This single prerequisite eliminates the overwhelming majority of undici installs from the vulnerable population. - Triple-prerequisite chain: Exploitation requires (1) attacker-controlled upstream, (2) retry interceptor enabled, AND (3) a forwarding proxy pattern that pipes headers verbatim. Each gate compounds the difficulty.
- Impact ceiling is low: The worst outcome is HTTP response desynchronization — stale
Content-Lengthcausing downstream connection confusion, truncation, or cache poisoning on individual connections. No code execution, no privilege escalation, no data exfiltration path. - Role multiplier: Undici is a general-purpose HTTP client library used across all tiers. In the *high-value role* of a Node.js API gateway or reverse proxy: the chain can succeed IF the gateway uses undici's retry interceptor and pipes headers verbatim (unusual — most gateway frameworks use chunked encoding or recalculate Content-Length). Blast radius on such a gateway would be per-connection response corruption, not fleet compromise or domain takeover. This does not cross the HIGH floor threshold because the outcome does not reach identity/fleet/supply-chain scale even in the worst plausible role.
- No exploitation evidence: Zero days in the wild, no PoC, no KEV listing, no scanner detections. The CVE is < 24 hours old with high attack complexity — weaponization pressure is low.
Why not higher?
Upgrading to HIGH would require either active exploitation evidence, a public weaponized PoC, or a blast radius that reaches fleet/domain/supply-chain scale. None of those conditions exist. The opt-in nature of the retry interceptor and the per-connection (not per-fleet) impact ceiling make HIGH unjustifiable. Even in a Node.js gateway role, the chain ends at response confusion, not system compromise.
Why not lower?
Downgrading to LOW would undervalue the real (if narrow) HTTP smuggling / desync risk for teams that *do* run Node.js forwarding proxies with the retry interceptor enabled. CWE-444 desync bugs have historically been chained for cache poisoning and request routing attacks. The network-reachable vector (AV:N) and no-auth requirement (PR:N) mean the attacker doesn't need prior access — only upstream control. MEDIUM correctly reflects the conditional-but-real risk.
What to do — in priority order.
- Audit codebases for
interceptors.retryusage — Rungrep -r 'interceptors.retry\|RetryAgent\|RetryHandler' --include='*.js' --include='*.ts'across your Node.js services. If no hits, you are not exposed to this CVE. This is the fastest triage step and should be done immediately to scope the problem. - Disable retry interceptor for untrusted upstreams — If you use the retry interceptor, disable it for any Dispatcher that talks to external or semi-trusted origins. This eliminates the attack surface without changing application behavior for trusted internal calls. Deploy within the 365-day noisgate remediation SLA window.
- Recalculate or strip Content-Length before forwarding — In proxy/gateway code, delete the upstream
Content-Lengthheader and let the framework recalculate it or switch toTransfer-Encoding: chunked. This neutralizes the stale-header vector regardless of undici version. Deploy as standard practice. - Upgrade undici to 6.28.0 / 7.29.0 / 8.9.0 or Node.js to 22.23.2 / 24.18.1 / 26.5.1 — The definitive fix. Prioritize within the 365-day noisgate remediation SLA. If you confirmed retry interceptor usage in step 1, accelerate to your next maintenance window.
- WAF rules for HTTP smuggling — most WAF smuggling signatures target *request*-side CL/TE mismatches between client and server. This bug produces a *response*-side Content-Length desync between the proxy and its downstream client, which standard WAF rulesets do not inspect.
- Upgrading Node.js without checking npm-installed undici — if your application installs undici as a direct dependency (not using the Node.js-bundled version), upgrading Node.js alone won't fix the vulnerability. You must also update the npm package.
- Rate-limiting or IP blocking — the attacker is the *upstream* origin, not a client sending requests. Inbound rate limits and IP ACLs are irrelevant to this attack vector.
Crowdsourced verification payload.
Run this on each host or in CI where your Node.js applications are deployed. It checks both npm-installed undici packages and the Node.js-bundled version. No special privileges required — just the node and npm (or npx) binaries in PATH. Example: bash check_cve_2026_16728.sh /path/to/your/app
#!/usr/bin/env bash
# check_cve_2026_16728.sh — Detect CVE-2026-16728 (undici retry interceptor Content-Length desync)
# Usage: bash check_cve_2026_16728.sh [app_directory]
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
APP_DIR="${1:-.}"
VULNERABLE=0
version_gte() {
# Returns 0 if $1 >= $2 using semantic versioning
printf '%s\n%s' "$2" "$1" | sort -V -C
}
echo "=== CVE-2026-16728 Check ==="
echo
# 1. Check Node.js bundled undici
if command -v node &>/dev/null; then
NODE_VER=$(node -v | sed 's/^v//')
BUNDLED_UNDICI=$(node -e "try{console.log(process.versions.undici||'none')}catch(e){console.log('none')}" 2>/dev/null || echo 'none')
echo "[*] Node.js version: $NODE_VER"
echo "[*] Bundled undici: $BUNDLED_UNDICI"
if [ "$BUNDLED_UNDICI" != "none" ]; then
MAJOR=$(echo "$BUNDLED_UNDICI" | cut -d. -f1)
case $MAJOR in
6) if version_gte "$BUNDLED_UNDICI" "6.28.0"; then echo " -> Bundled undici PATCHED"; else echo " -> Bundled undici VULNERABLE"; VULNERABLE=1; fi ;;
7) if version_gte "$BUNDLED_UNDICI" "7.29.0"; then echo " -> Bundled undici PATCHED"; else echo " -> Bundled undici VULNERABLE"; VULNERABLE=1; fi ;;
8) if version_gte "$BUNDLED_UNDICI" "8.9.0"; then echo " -> Bundled undici PATCHED"; else echo " -> Bundled undici VULNERABLE"; VULNERABLE=1; fi ;;
*) echo " -> Bundled undici version $MAJOR.x not in affected range"; ;;
esac
fi
else
echo "[!] node not found in PATH"
fi
echo
# 2. Check npm-installed undici in the app directory
if [ -d "$APP_DIR/node_modules/undici" ]; then
PKG_VER=$(node -e "console.log(require('$APP_DIR/node_modules/undici/package.json').version)" 2>/dev/null || echo 'unknown')
echo "[*] npm undici in $APP_DIR: $PKG_VER"
if [ "$PKG_VER" = "unknown" ]; then
echo " -> UNKNOWN (could not read version)"
else
MAJOR=$(echo "$PKG_VER" | cut -d. -f1)
case $MAJOR in
[0-5]) echo " -> Version $PKG_VER predates affected retry interceptor (likely not vulnerable, but upgrade recommended)"; ;;
6) if version_gte "$PKG_VER" "6.28.0"; then echo " -> npm undici PATCHED"; else echo " -> npm undici VULNERABLE"; VULNERABLE=1; fi ;;
7) if version_gte "$PKG_VER" "7.29.0"; then echo " -> npm undici PATCHED"; else echo " -> npm undici VULNERABLE"; VULNERABLE=1; fi ;;
8) if version_gte "$PKG_VER" "8.9.0"; then echo " -> npm undici PATCHED"; else echo " -> npm undici VULNERABLE"; VULNERABLE=1; fi ;;
*) echo " -> Version $MAJOR.x — check advisory for coverage"; ;;
esac
fi
else
echo "[*] No npm-installed undici found in $APP_DIR/node_modules/"
fi
echo
# 3. Check for retry interceptor usage (exposure gate)
echo "[*] Scanning for retry interceptor usage in $APP_DIR ..."
if grep -r -l 'interceptors\.retry\|RetryAgent\|RetryHandler' --include='*.js' --include='*.ts' --include='*.mjs' --include='*.cjs' "$APP_DIR" 2>/dev/null | grep -v node_modules | head -5; then
echo " -> RETRY INTERCEPTOR IN USE — this app may be exposed"
else
echo " -> No retry interceptor usage found (app likely not exposed even if undici is vulnerable)"
fi
echo
if [ "$VULNERABLE" -eq 1 ]; then
echo "VULNERABLE"
exit 1
else
echo "PATCHED"
exit 0
fiIf you remember one thing.
interceptors.retry / RetryAgent / RetryHandler across your Node.js estate; if you get zero hits, you are not exposed and this can ride your next scheduled dependency update. If you *do* find retry interceptor usage in a forwarding proxy or gateway service, bump that service's undici to 6.28.0 / 7.29.0 / 8.9.0 (or update Node.js to 22.23.2 / 24.18.1 / 26.5.1) in your next sprint. There is no active exploitation, no public PoC, and no KEV listing — this is a routine library-level patch, not a fire drill.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.