CVE Patch Review

CVE-2026-54448: Trivy Helm Parser Moves from Recursive Unpack to Direct Archive Parsing

CVE-2026-54448 · Updated 2026-07-15 Root-cause

Summary

The patch removes the custom recursive archive unpacking path that performed unbounded io.ReadAll() on tar entries into an in-memory filesystem, and replaces archive handling with direct ParseArchive() processing via archive.LoadArchiveFiles(). This is a strong architectural correction for the Helm scanner entrypoint because it avoids the vulnerable unpackArchive() flow entirely for archive targets, but the provided diff does not expose whether archive.LoadArchiveFiles() itself enforces decompression and per-entry size limits. Based on the visible changes, the fix appears root-cause-oriented for the affected parser path, with residual assurance depending on the archive library's safeguards.

Analysis

Vulnerability

CVE-2026-54448 describes a denial-of-service condition in Trivy's Helm chart parsing where small malicious archives can trigger excessive memory consumption during decompression and crash the scanner process. The vulnerable implementation used a custom archive unpacker that recursively expanded archives into an in-memory filesystem.

The critical issue is visible in the old unpacking logic from the Trivy patch reference PR #10718. For regular tar entries, the parser read the entire decompressed payload into memory with no visible size bound before writing it into mapfs:

case tar.TypeReg:
	data, err := io.ReadAll(tr)
	if err != nil {
		return fmt.Errorf("read file: %w", err)
	}

	p.logger.Debug("Unpacking tar entry", log.FilePath(targetPath))
	if err := writeFile(targetFS, data, targetPath); err != nil {
		return err
	}

This design compounds risk in two ways: first, decompressed tar members are materialized with unbounded io.ReadAll(); second, the extracted content is persisted into an in-memory filesystem, so attacker-controlled expansion directly consumes heap. The old parser also recursively re-entered archive parsing through parseFS(), increasing the blast radius for nested or repeated archive processing.

The CVE context from MITRE and NVD aligns with this root cause: decompression bombs exploit unbounded expansion during archive handling, leading to memory exhaustion rather than code execution or data corruption.

Patch

The patch changes the Helm scanner flow so archive targets are no longer processed through the custom recursive unpacker. Instead, the scanner detects archive input and routes it to a dedicated archive parser:

if detection.IsArchive(path) {
	if err := helmParser.ParseArchive(ctx, target, path); err != nil {
		return nil, err
	}
} else if err := helmParser.ParseFS(ctx, target, path); err != nil {

The new ParseArchive() implementation opens the archive and delegates loading to a shared archive utility:

func (p *Parser) ParseArchive(_ context.Context, fsys fs.FS, archivePath string) error {
	f, err := fsys.Open(archivePath)
	if err != nil {
		return fmt.Errorf("open archive: %w", err)
	}
	defer f.Close()

	files, err := archive.LoadArchiveFiles(f)
	if err != nil {
		return fmt.Errorf("load archive files: %w", err)
	}

	for _, file := range files {
		if file.Name == "Chart.yaml" {
			p.applyChartName(file.Data, "")
			break
		}
	}

	p.archiveFiles = files
	return nil
}

In parallel, the old unpackArchive() path shown in the vulnerable snippet is absent from the patched flow, and scanner-side logic adds shouldSkipArchive() checks to avoid redundant processing when an unpacked chart already exists next to an archive. Tests were updated to exercise ParseArchive() directly and to use .tar.gz fixtures consistent with the gzip/tar handling path referenced in the patch summary.

Functionally, this is an architectural simplification: instead of recursively inflating archives into mapfs, the parser now consumes a buffered archive file list and reuses that list when building Helm input files.

Review

Pros

  • Removes the visible vulnerable primitive: the custom unpacker that performed unbounded io.ReadAll() on decompressed tar entries into memory-backed storage.
  • Eliminates recursive archive-to-filesystem expansion in the Helm parser path, which was the direct mechanism enabling decompression bomb amplification.
  • Introduces a cleaner separation between filesystem parsing and archive parsing, reducing parser complexity and making archive handling easier to reason about.
  • Preserves chart name extraction by parsing Chart.yaml directly from archive-loaded file buffers, avoiding the need to materialize a synthetic filesystem solely for metadata discovery.
  • Adds skip logic for archives adjacent to unpacked charts, which reduces duplicate work and lowers accidental resource consumption during normal scans.

Cons

  • The visible diff does not show the implementation of archive.LoadArchiveFiles(), so the review cannot verify whether that helper enforces maximum decompressed size, maximum entry size, maximum file count, or nesting limits.
  • The new path still appears to buffer archive contents into memory as []*archive.BufferedFile. If the shared archive loader lacks hard limits, the DoS condition could migrate rather than disappear.
  • unpackedChartExists() opens gzip/tar content and reads the first header without any explicit resource budget in the shown code. This is much smaller in scope than full extraction, but it is still archive parsing on attacker input.
  • The patch evidence is strong for the Helm scanner entrypoint, but the review cannot prove there are no remaining callers of the old unpacking logic elsewhere because only partial file context is provided.

Verdict

Root-cause.

Within the provided patch scope, the fix addresses the actual failure mode by removing the custom archive unpack-and-recurse design that used unbounded reads into an in-memory filesystem. That is the right security direction for a decompression bomb issue. However, confidence is conditional on the shared archive loader used by ParseArchive(). If archive.LoadArchiveFiles() implements decompression and buffering limits, this is a robust remediation; if it does not, the patch is still structurally improved but may retain memory-exhaustion exposure in a different helper. Engineers should verify the shared archive library for explicit caps on decompressed bytes, per-file size, file count, and nested archive handling before treating the issue as comprehensively closed.

Sources: GitHub patch reference, NVD CVE entry, MITRE CVE record, advisory summary.

Sources