Skip to content

Back to the catalog

Endor Labs AI SAST Finds Two Apache Kafka Zero-Days: Unbounded Memory Allocation from Brokers to Kafka Streams | Endor Labs
endorlabs.comwriteup

Endor Labs AI SAST Finds Two Apache Kafka Zero-Days: Unbounded Memory Allocation from Brokers to Kafka Streams | Endor Labs

Endor Labs AI SAST found three unbounded memory allocation flaws in Apache Kafka, fixed under CVE-2026-114349 (DefaultRecord decoder, a ~90-byte compressed batch that kills consumers, brokers and the log cleaner) and CVE-2026-111551 (ListDeserializer and Kafka Streams AssignmentInfo decoding). The post gives code, reproducers and payload bytes. Remediation: upgrade to 4.2.2, 4.3.2 or 4.4.0, set max.decompressed.message.bytes, restrict write and group ACLs, and alert on DeadThreadCount.

TL;DR Endor Labs details two Kafka CVEs where untrusted size fields drive unbounded allocations, crashing consumers, Streams apps and the log cleaner.
Document

What it is

Two Apache Kafka CVEs, both rated important, found by Endor Labs' AI SAST in a single scan on May 1, 2026. Both are unbounded allocations: code reads a size or count from the input and allocates before checking it against the bytes present. The Kafka project published both advisories on October 28, 2026.

  • CVE-2026-114349: DefaultRecord.readFrom (InputStream overload, line 286) allocates a record body sized by a varint inside the compressed data. A batch of about 90 bytes can declare roughly 2 GiB.
  • CVE-2026-111551: ListDeserializer (new ArrayList<>(nullIndexListSize), plus a second site at line 179) and Kafka Streams AssignmentInfo decoders (decodeActiveTasks and four siblings).

How it works

Record size limits, max.message.bytes and quotas all measure the compressed or wire bytes, so none of them bounds what the payload declares. The producer computes the batch CRC, so the crafted batch is CRC-valid.

flowchart TD
    A[Crafted ~90-byte batch produced] --> B{Compression matches topic?}
    B -- Yes --> C[Stored and replicated unchanged]
    B -- No --> D[Broker re-encodes: handler OOM]
    C --> E[Consumers OOM on every fetch]
    C --> F[Log cleaner thread dies]
    D --> G[Broker JVM exits if ExitOnOutOfMemoryError]
  • Cleaner: the OutOfMemoryError escapes error handling and the thread is not restarted. With the default single cleaner thread, compaction stops on all compacted topics, including __consumer_offsets and __transaction_state. The broker still passes health checks.
  • ListDeserializer: a 9-byte value from a stock KafkaProducer, 00 7F FF FF FF 00 00 00 00, crashes every consumer using ListDeserializer on its next poll and after each restart.
  • Streams assignment: a plain consumer can register an assignor named stream, join a Streams app's group, win leader election and return arbitrary assignment bytes. The 12 bytes 00 00 00 0B 00 00 00 0B 7F FF FF FF crashed every stream thread, under both eager and cooperative rebalancing. The attacker stays in the group, so each restart crashes again.

Preconditions / Affected

  • CVE-2026-114349: Write access to a topic. Cleaner impact needs compacted topics. Broker JVM exit needs mismatched compression plus -XX:+ExitOnOutOfMemoryError.
  • ListDeserializer: write access to topics read with ListDeserializer or ListSerde, including Streams pipelines, connectors and wrappers.
  • Streams assignment: only the Group:Read ACL on the group. The post says this matters most in multi-tenant clusters with wildcard group ACLs.
  • The fix also covers four more Streams paths: ProcessorRecordContext, ChangedDeserializer, CombinedKeySchema and ProcessorMetadata. These read internal changelog and repartition topics and committed offset metadata, so write access there counts as attack surface.
  • Fixed in 4.2.2, 4.3.2 and 4.4.0.

What to do

  1. Upgrade to 4.2.2, 4.3.2 or 4.4.0.
  2. Set max.decompressed.message.bytes at broker or topic level. Upgrading alone does not protect against CVE-2026-114349, because the default stays near 2 GiB for compatibility. Pick a value at or slightly above your largest legitimate decompressed record, and well below broker heap divided by request-handler plus cleaner threads.
  3. Until upgraded, limit Write access to trusted producers. Restrict each Streams app's internal topics and consumer group to its own principal, and restrict who can produce to ListSerde topics.
  4. Alert on this metric:
kafka.log:type=LogCleaner,name=DeadThreadCount > 0

Caveats

  • Quotas and max.message.bytes do not mitigate either issue.
  • A crafted record already in a topic stays a hazard for consumers until it is removed or ages out, even after the limit is set.
  • The author reproduced the attacks against apache/kafka:4.2.0. Reproducer code is described, not published in full.

Organisations named here

  • DefaultRecord
  • ListDeserializer
  • max.decompressed.message.bytes
  • OutOfMemoryError

CVEs named here

  • CVE-2026-114349
  • CVE-2026-111551

Technologies named here

  • Apache Kafka
  • Custom raw Produce request reproducer
  • JMX metric kafka.log:type=LogCleaner,name=DeadThreadCount
  • Java
  • Endor Labs
  • Kafka Streams

Techniques named here

  • Disclosure timeline
  • Denial of service