Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

README.md

CVE-2026-63202 — Netty BinaryHttpParser unauthenticated CPU-exhaustion DoS

  • Advisory: GHSA-4899-mpch-38p3 · CVE-2026-63202
  • Severity: High · CVSS 3.1 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
  • CWE: CWE-835 (Loop with Unreachable Exit Condition) / CWE-400 (Uncontrolled Resource Consumption)
  • Affected: io.netty.incubator:netty-incubator-codec-bhttp ≤ 0.0.22.Final — fixed in 0.0.23.Final
  • Impact: an unauthenticated attacker permanently pins a Netty event-loop thread at 100% CPU with a single ~17-byte message; a handful of requests takes an OHTTP gateway/client fully offline.

Root cause

BinaryHttpParser.readFieldSection decodes the Binary HTTP (RFC 9292) field section in a loop whose only exit condition is fieldSectionLength != 0:

while (fieldSectionLength != 0) {            // "!= 0", not "> 0"
    int readableBytes = in.readableBytes();
    lastType = readFieldLine(in, headers, lastType, trailers);
    assert lastType != null;                 // no-op without -ea
    int read = readableBytes - in.readableBytes();
    assert read > 0;                         // no-op without -ea
    fieldSectionLength -= read;
}

Two cooperating defects let the loop spin forever:

  1. The counter can never reach zero. fieldSectionLength is the declared field-section length read from the wire. The loop subtracts the bytes each readFieldLine actually consumed. If a field line consumes more bytes than the (attacker-understated) declared length, fieldSectionLength goes negative and != 0 stays true forever.
  2. Zero-progress iterations. readFieldLine returns null without consuming any bytes when the remaining buffer cannot hold a complete field line (the skipBytes that advances the reader is only reached on the success path). read == 0, the counter is unchanged, and the loop re-enters with identical state — a tight busy-spin.

The two assert statements that would have caught either case are stripped by the JVM unless it is started with -ea. Production JVMs run with assertions disabled, so there is no guard at all.

Reachability (unauthenticated)

OHTTP gateways publish their HPKE key configuration so any client can encrypt requests to them. The attacker encrypts a malicious BHTTP body under the gateway's public key — a perfectly valid OHTTP request. HPKE decapsulation succeeds and the plaintext is attacker-chosen. It flows straight into the parser with no application code in between:

OHttpServerCodec.decode
  → OHttpRequestResponseContext.parse
    → ContentDecoder.decodeChunk        (decrypts the chunk)
      → binaryHttpParser.parse(...)      (attacker-controlled plaintext)
        → readRequestHead → readFieldSection   ← infinite loop

maxFieldSectionSize (default 8 KiB for the OHTTP codecs) is irrelevant: the declared length in the PoC is 1, and the spin is CPU-bound on a fixed 17-byte buffer with no allocation.

PoC

poc/run.sh downloads the affected released jar (netty-incubator-codec-bhttp:0.0.22.Final) plus its Netty deps from Maven Central, then runs:

  • Poc.java — builds a valid known-length BHTTP request whose declared field-section length (0x01) is understated relative to the actual field line, calls parse(in, true) on a worker thread with a 6-second watchdog, and reports the hang + CPU time + thread stack. Benign: a pure liveness oracle, no payload, no side effects.
  • Poc2.java — a well-formed message (declared length matches), proving the harness does not hang on valid input.
cd poc
./run.sh

Malicious message (17 bytes): 0001670168016101700101610162016301

Observed against 0.0.22.Final (assertions off, i.e. the production default):

=== negative control (well-formed input, must return promptly) ===
[OK] well-formed parse returned: DefaultBinaryHttpRequest
[--] returned promptly (no hang)

=== exploit (malicious input, must HANG at ~100% CPU) ===
[*] malicious BHTTP bytes (17): 0001670168016101700101610162016301
[!!] HANG CONFIRMED: parse() still running after 6001 ms
[!!] worker thread CPU time: 6037 ms (≈100% of one core => busy spin)
[!!] worker stack (top frames):
        at io.netty.incubator.codec.bhttp.BinaryHttpParser.readFieldSection(BinaryHttpParser.java:626)
        at io.netty.incubator.codec.bhttp.BinaryHttpParser.readRequestHead(BinaryHttpParser.java:451)
        at io.netty.incubator.codec.bhttp.BinaryHttpParser.parse(BinaryHttpParser.java:190)
[+] CVE-2026-63202 reproduced: parse() busy-spun on the 17-byte message (exit 42).

CPU time ≈ wall time ⇒ a busy spin (RUNNABLE), not a blocked wait. Running the same input with -ea throws AssertionError at readFieldSection immediately, confirming the assertion is the only would-be guard and is absent in production.

Fix

Fixed in 0.0.23.Final. The loop exit becomes while (fieldSectionLength > 0), a null / zero-progress return from readFieldLine is treated as a hard framing error (CorruptedFrameException), and a field line that would drive the counter below zero is rejected — i.e. wire-format invariants on attacker-controlled input are enforced with real exceptions instead of assert.

Not the same as CVE-2024-40642

That advisory covered absent input validation of the method/scheme/authority/path (enabling injection); its fix (the ALLOWED_TOKEN / ALLOWED_SCHEME validators) is present and unrelated. This is a distinct control-flow / termination defect in field-section length accounting.