← Back to Feed CACHED · 2026-07-30 08:53:01 · CACHE_KEY CVE-2026-16728
CVE-2026-16728 · CWE-444 · Disclosed 2026-07-29

undici's retry interceptor can deliver a response whose body length does not match the Content-Length…

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
Do you agree?
01 · The Real Story

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.

"Opt-in retry interceptor plus proxy pattern keeps this HTTP desync bug a niche MEDIUM."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Attacker controls or compromises an upstream origin

The attacker must be positioned as (or have compromised) a server that the vulnerable undici client connects to. This is the prerequisite that gates the entire chain — without upstream control, the mismatched Content-Length + early socket close cannot be injected.
Conditions required:
  • Attacker controls or has compromised an HTTP origin the undici client fetches from
Where this breaks in practice:
  • 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
STEP 02

Undici retry interceptor is enabled on the client

The application must have explicitly opted into 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.
Conditions required:
  • Application code explicitly configures interceptors.retry() on the Dispatcher
Where this breaks in practice:
  • 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
Detection/coverage: Static analysis / grep for interceptors.retry in the codebase
STEP 03

Upstream returns partial response with stale Content-Length and drops connection

The malicious upstream sends an HTTP response (e.g., 200 or 206) with a 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.
Conditions required:
  • 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
Where this breaks in practice:
  • 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)
STEP 04

Application forwards response with stale Content-Length downstream

The vulnerable application acts as a proxy or gateway: it reads the response from undici (headers + body) and writes them to a downstream HTTP connection without recalculating 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.
Conditions required:
  • Application is a forwarding proxy / API gateway that pipes headers verbatim
  • Downstream connection is persistent (HTTP/1.1 keep-alive) so desync actually matters
Where this breaks in practice:
  • 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
Detection/coverage: WAF or reverse proxy in front of the gateway may flag Content-Length / body mismatch as a smuggling anomaly
03 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNone 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-conceptNo 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.
EPSSNot 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 statusNot listed. No CISA KEV entry as of 2026-07-30.
CVSS vectorCVSS: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 versionsundici < 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 versionsundici 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 dataUndici 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 date2026-07-29 via GitHub Security Advisory GHSA-8xcm-r25x-g524.
Reporter / creditssamuel871211 (reporter), mcollina (Matteo Collina, remediation developer), UlisesGascon (remediation reviewer).
04 · The Call

noisgate verdict.

Final Verdict
= UNCHANGED to MEDIUM (4.8/10)

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.

HIGH Vulnerability mechanism and affected version accuracy
HIGH Vendor severity alignment
MEDIUM Exploitable population estimate (no telemetry on retry interceptor adoption)

Why this verdict

  • Opt-in feature gate: interceptors.retry() must be explicitly wired up by the developer. Node.js's built-in fetch() 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-Length causing 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.

05 · Compensating Control

What to do — in priority order.

  1. Audit codebases for interceptors.retry usage — Run grep -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.
  2. 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.
  3. Recalculate or strip Content-Length before forwarding — In proxy/gateway code, delete the upstream Content-Length header and let the framework recalculate it or switch to Transfer-Encoding: chunked. This neutralizes the stale-header vector regardless of undici version. Deploy as standard practice.
  4. 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.
What doesn't work
  • 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.
06 · Verification

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

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/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
fi
07 · Bottom Line

If you remember one thing.

TL;DR
This is a MEDIUM with no mitigation SLA under noisgate policy — go straight to the 365-day remediation window. Monday morning, run the grep for 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

  1. GitHub Security Advisory GHSA-8xcm-r25x-g524
  2. THREATINT CVE-2026-16728 Entry
  3. Node.js July 29 2026 Security Releases
  4. undici npm package
  5. Undici vs Built-in Fetch — Node.js Docs
  6. Socket.dev undici Security Analysis
  7. Snyk undici Vulnerability Database
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.