GitHub Advisoryadvisory · Maven
GHSA-4899-mpch-38p3 netty-incubator-codec-ohttp BinaryHttpParser: Unauthenticated CPU-exhaustion DoS via infinite loop in field-section decoding
HIGH
Official page ↗
- Aliases
- CVE-2026-63202
- Ecosystem
- Maven
- Packages
- io.netty.incubator:netty-incubator-codec-bhttp
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-400, CWE-835
- Reviewed
- 2026-08-20
Affected packages
• Maven io.netty.incubator:netty-incubator-codec-bhttp: < 0.0.23.Final → fixed in 0.0.23.Final
Description
BinaryHttpParser: Unauthenticated CPU-exhaustion DoS via infinite loop in field-section decoding
• ID: BHTTP-LOOP-001
• Severity: High
• CVSS v3.1: 7.5 — `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H`
• CWE: CWE-835 (Loop with Unreachable Exit Condition) — secondary CWE-400 (Uncontrolled Resource Consumption)
• Affected component: `codec-bhttp` → `io.netty.incubator.codec.bhttp.BinaryHttpParser#readFieldSection`, file `codec-bhttp/src/main/java/io/netty/incubator/codec/bhttp/BinaryHttpParser.java:619-626`
• Affected version: netty-incubator-codec-ohttp HEAD `d3f2b49` (release `0.0.22.Final` + 3 commits). The loop has existed since the parser was introduced and is present in the latest code; all published advisory fixes are already applied.
• Reachable from: `io.netty.incubator.codec.ohttp.OHttpRequestResponseContext$ContentDecoder#decodeChunk` (`codec-ohttp/.../OHttpRequestResponseContext.java:214`), i.e. the auto-wired OHTTP server and client codecs.
• Confidence: High (empirically reproduced hang + thread dump against the unmodified parser).
Summary
`BinaryHttpParser` decodes Binary HTTP (RFC 9292) messages. An OHTTP gateway/client built on this library feeds the decrypted OHTTP body straight into `BinaryHttpParser.parse(...)`. The field-section decoding loop terminates only on the exact condition `fieldSectionLength != 0` and relies on a Java `assert` to guarantee forward progress. Because (a) the loop counter can be driven negative and (b) `readFieldLine(...)` legitimately consumes zero bytes and returns `null` on a truncated/over-long field line, the loop can spin forever. Assertions are disabled in any normal production JVM, so the two `assert` statements meant to catch this provide no protection.
A single ~17-byte Binary HTTP message — encapsulated by an unauthenticated attacker inside a normal OHTTP request, using the gateway's public key configuration — pins one Netty event-loop thread at 100% CPU permanently. A handful of such requests exhausts the entire event-loop group and takes the OHTTP gateway (or client) fully offline.
Root cause
`BinaryHttpParser.java:619-626`:
```java
HeaderType lastType = HeaderType.PSEUDO_HEADER;
while (fieldSectionLength != 0) { // 619 — "!= 0", not "> 0"
int readableBytes = in.readableBytes();
lastType = readFieldLine(in, headers, lastType, trailers);
assert lastType != null; // 622 — no-op without -ea
int read = readableBytes - in.readableBytes();
assert read > 0; // 624 — no-op without -ea
fieldSectionLength -= read; // 625
}
```
Two cooperating defects:
1. Counter can never hit zero. `fieldSectionLength` is the *declared* field-section byte length read from the wire (line 592). The loop subtracts the bytes each `readFieldLine` actually consumes. 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` (lines 654-707) returns `null` without consuming any bytes when the remaining buffer cannot hold a complete field line — at lines 656, 664, 670, and 681 (the `in.skipBytes(sumBytes)` that advances the reader is only reached on the success path, line 705). When it returns `null`, `read == 0`, `fieldSectionLength` is unchanged, and the loop re-enters with identical state — a tight busy spin.
The only constructs that would have stopped either case are the `assert` statements on lines 622 and 624, which the JVM strips unless started with `-ea`. Production deployments do not run with assertions enabled.
Reachability (hop-by-hop, every guard resolved)
Attacker model: OHTTP gateways publish their HPKE key configuration so that *any* client can encrypt requests to them. The attacker therefore encrypts a malicious BHTTP body under the gateway's public key — a perfectly valid OHTTP request. HPKE decapsulation succeeds; the plaintext is attacker-chosen.
1. `OHttpServerCodec.decode` → `OHttpRequestResponseContext.parse` → chunk decode → `ContentDecoder.decodeChunk`.
2. `OHttpRequestResponseContext.java:211` decrypts the chunk into `decryptedChunk`; line 212 cumulates it; line 214 calls `binaryHttpParser.parse(binaryHttpCumulation, completeBodyReceived)` — attacker-controlled plaintext, no application code in between.
3. `parse` → `READ_KNOWN_LENGTH_REQUEST_HEAD` → `readRequestHead` (line 190).
4. `readRequestHead` reads the control data, then at lines 445-451 slices all remaining readable bytes as the field section and calls `readFieldSection(..., knownLength=true, maxFieldSectionSize)`.
5. Inside `readFieldSection`:
• Guard `checkFieldSectionTooLarge(fieldSectionLength, max)` (line 607): bounds only the *declared* length, which the PoC sets to `1`. Passes — not a barrier.
• Guard `in.readableBytes() 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)
```
CPU time ≈ wall time ⇒ a busy spin (RUNNABLE), not a blocked wait.
Controls:
• Same input with `-ea`: `parse()` throws `AssertionError` at `readFieldSection:624` immediately — proving the assertion is the only would-be guard and is absent in production.
• Well-formed request (declared length matches):** `parse()` returns `DefaultBinaryHttpRequest` promptly — proving the harness does not hang on valid input.
PoC sources: `findings/netty-incubator-codec-ohttp/raw/Poc.java` (hang + control 1) and `raw/Poc2.java` (negative control).
Impact
Unauthenticated, pre-business-logic remote denial of service. Each malicious request permanently consumes one Netty event-loop thread at 100% CPU. Netty event-loop groups have a small fixed thread count (default `2 × cores`); a handful of requests exhausts every I/O thread, after which the gateway/client accepts no further connections and serves no traffic — a complete, persistent DoS that survives until process restart. Availability impact High; no confidentiality/integrity impact.
Adversarial re-reading (attempts to refute)
• *"`maxFieldSectionSize` caps it."* No — the declared length in the PoC is `1`; the cap (line 607) checks the declared value only. The spin happens on a 17-byte buffer with no allocation. …