- 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.
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:
- The counter can never reach zero.
fieldSectionLengthis the declared field-section length read from the wire. The loop subtracts the bytes eachreadFieldLineactually consumed. If a field line consumes more bytes than the (attacker-understated) declared length,fieldSectionLengthgoes negative and!= 0stays true forever. - Zero-progress iterations.
readFieldLinereturnsnullwithout consuming any bytes when the remaining buffer cannot hold a complete field line (theskipBytesthat 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.
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/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, callsparse(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.shMalicious 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.
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.
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.