GitHub Security Advisory: GHSA-f6hv-jmp6-3vwv
Package: io.netty:netty-codec-http2 (MAVEN)
Severity: HIGH (CVSS 7.5)
CWE: CWE-400: Uncontrolled Resource Consumption
Vulnerable versions: >= 4.2.0.Alpha1, <= 4.2.12.Final
Patched in: 4.2.13.Final

Summary: Netty: HttpContentDecompressor maxAllocation bypass when Content-Encoding set to br/zstd/snappy leads to decompression bomb DoS

Details:
## Summary

`HttpContentDecompressor` accepts a `maxAllocation` parameter to limit decompression buffer size and prevent decompression bomb attacks. This limit is correctly enforced for gzip and deflate encodings via `ZlibDecoder`, but is silently ignored when the content encoding is `br` (Brotli), `zstd`, or `snappy`. An attacker can bypass the configured decompression limit by sending a compressed payload with `Content-Encoding: br` instead of `Content-Encoding: gzip`, causing unbounded memory allocation and out-of-memory denial of service.

The same vulnerability exists in `DelegatingDecompressorFrameListener` for HTTP/2 connections.

## Details

`HttpContentDecompressor` stores the `maxAllocation` value at construction time (`HttpContentDecompressor.java:89`) and uses it in `newContentDecoder()` to create the appropriate decompression handler.

For gzip/deflate, `maxAllocation` is forwarded to `ZlibCodecFactory.newZlibDecoder()`:

```java
// HttpContentDecompressor.java:101 — maxAllocation IS enforced
.handlers(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP, maxAllocation))
```

`ZlibDecoder.prepareDecompressBuffer()` enforces this as a hard cap by setting the buffer's `maxCapacity` and throwing `DecompressionException` when the limit is reached:

```java
// ZlibDecoder.java:68 — hard limit on buffer capacity
return ctx.alloc().heapBuffer(Math.min(preferredSize, maxAllocation), maxAllocation);
// ZlibDecoder.java:80 — throws when exceeded
throw new DecompressionException("Decompression buffer has reached maximum size: " + buffer.maxCapacity());
```

For brotli, zstd, and snappy, the decoders are created without any size limit:

```java
// HttpContentDecompressor.java:120 — maxAllocation IGNORED
.handlers(new BrotliDecoder())

// HttpContentDecompressor.java:129 — maxAllocation IGNORED
.handlers(new SnappyFrameDecoder())

// HttpContentDecompressor.java:138 — maxAllocation IGNORED
.handlers(new ZstdDecoder())
```

`BrotliDecoder` has no `maxAllocation` parameter at all — there is no way to constrain its output. It streams decompressed data in chunks via `fireChannelRead` with no total limit.

`ZstdDecoder()` defaults to a 4MB `maximumAllocationSize`, but this only constrains individual buffer allocations, not total output. The decode loop (`ZstdDecoder.java:100-114`) creates new buffers and fires `channelRead` repeatedly, so total decompressed output is unbounded.

The identical pattern exists in `DelegatingDecompressorFrameListener.newContentDecompressor()` at lines 188-210 for HTTP/2.

## PoC

1. Configure a Netty HTTP server with decompression bomb protection:

```java
pipeline.addLast(new HttpContentDecompressor(1048576)); // 1MB max
pipeline.addLast(new HttpObjectAggregator(1048576));     // 1MB max
```

2. Generate a brotli-compressed bomb (~1KB compressed → 1GB decompressed):

```python
import brotli
bomb = b'\x00' * (1024 * 1024 * 1024)  # 1GB of zeros
compressed = brotli.compress(bomb, quality=11)
with open('bomb.br', 'wb') as f: