Moderate severityNVD Advisory· Published Feb 27, 2026· Updated Mar 2, 2026
malcontent's nested archive extraction failure can drop content from scan inputs
CVE-2026-28407
Description
malcontent is software for discovering supply-chain compromises through context, differential analysis, and YARA. Prior to version 1.21.0, malcontent would remove nested archives which failed to extract which could potentially leave malicious content. A better approach is to preserve these archives so that malcontent can attempt a best-effort scan of the archive bytes. Version 1.21.0 fixes the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/chainguard-dev/malcontentGo | < 1.21.0 | 1.21.0 |
Affected products
1- Range: < 1.21.0
Patches
1356c56659ccffix: preserve nested archives which fail to extract (#1383)
2 files changed · +117 −3
pkg/action/archive_test.go+110 −0 modified@@ -4,7 +4,9 @@ package action import ( + "archive/tar" "bytes" + "compress/gzip" "context" "io/fs" "os" @@ -592,6 +594,114 @@ func TestScanConflictingArchiveFiles(t *testing.T) { } } +// createBrokenNestedArchive creates a tar.gz file containing a nested +// file with an archive extension whose content is valid gzip but invalid tar. +func createBrokenNestedArchive(t *testing.T, dir string) string { + t.Helper() + + outPath := filepath.Join(dir, "outer.tar.gz") + f, err := os.Create(outPath) + if err != nil { + t.Fatalf("failed to create outer archive: %v", err) + } + defer f.Close() + + gw := gzip.NewWriter(f) + defer gw.Close() + tw := tar.NewWriter(gw) + defer tw.Close() + + var innerBuf bytes.Buffer + innerGw := gzip.NewWriter(&innerBuf) + if _, err := innerGw.Write(bytes.Repeat([]byte("A"), 1024)); err != nil { + t.Fatalf("failed to write inner gzip data: %v", err) + } + if err := innerGw.Close(); err != nil { + t.Fatalf("failed to close inner gzip writer: %v", err) + } + + innerData := innerBuf.Bytes() + if err := tw.WriteHeader(&tar.Header{ + Name: "bad_nested.tar.gz", + Mode: 0o600, + Size: int64(len(innerData)), + }); err != nil { + t.Fatalf("failed to write tar header: %v", err) + } + if _, err := tw.Write(innerData); err != nil { + t.Fatalf("failed to write tar data: %v", err) + } + + return outPath +} + +// TestNestedFailureRetention verifies that when a nested archive +// extraction fails with ExitExtraction=false (default), the original nested archive +// file is retained in the extraction directory for scanning rather than being deleted. +func TestNestedFailureRetention(t *testing.T) { + t.Parallel() + + tmpDir, err := os.MkdirTemp("", "nested-fail-retain-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + outerArchive := createBrokenNestedArchive(t, tmpDir) + + ctx := context.Background() + cfg := malcontent.Config{ExitExtraction: false} + + extractDir, err := archive.ExtractArchiveToTempDir(ctx, cfg, outerArchive) + if err != nil { + t.Fatalf("ExtractArchiveToTempDir should not fail with ExitExtraction=false, got: %v", err) + } + defer os.RemoveAll(extractDir) + + // The nested archive file must still exist so it can be scanned as a regular file + found := false + err = filepath.WalkDir(extractDir, func(_ string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.Name() == "bad_nested.tar.gz" { + found = true + } + return nil + }) + if err != nil { + t.Fatalf("failed to walk extraction directory: %v", err) + } + if !found { + t.Fatal("nested archive file was deleted after extraction failure but should be retained for scanning") + } +} + +// TestNestedFailureRetentionError verifies that when ExitExtraction=true, +// a nested archive extraction failure propagates as an error. +func TestNestedFailureRetentionError(t *testing.T) { + t.Parallel() + + tmpDir, err := os.MkdirTemp("", "nested-fail-exit-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + outerArchive := createBrokenNestedArchive(t, tmpDir) + + ctx := context.Background() + cfg := malcontent.Config{ExitExtraction: true} + + extractDir, err := archive.ExtractArchiveToTempDir(ctx, cfg, outerArchive) + if extractDir != "" { + defer os.RemoveAll(extractDir) + } + if err == nil { + t.Fatal("ExtractArchiveToTempDir should return error with ExitExtraction=true for nested archives which cannot be extracted") + } +} + func TestIsValidPath(t *testing.T) { tmpRoot, err := os.MkdirTemp("", "isValidPath-*") if err != nil {
pkg/archive/archive.go+7 −3 modified@@ -194,13 +194,17 @@ func extractNestedArchive(ctx context.Context, c malcontent.Config, d string, f if c.ExitExtraction { return fmt.Errorf("failed to extract archive: %w", err) } - logger.Debugf("ignoring extraction error for %s: %s", f, err.Error()) + logger.Warnf("extraction failed for %s, retaining archive for scanning: %s", f, err.Error()) } extracted.Store(f, true) - if err := os.Remove(fullPath); err != nil { - return fmt.Errorf("failed to remove archive file: %w", err) + // only attempt to remove the archive file if we don't encounter an extraction error + // any archives which cannot be extracted will be scanned like non-archive files + if err == nil { + if err := os.Remove(fullPath); err != nil { + return fmt.Errorf("failed to remove archive file: %w", err) + } } entries, err := os.ReadDir(d)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-945p-3jhm-6rcpghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-28407ghsaADVISORY
- github.com/chainguard-dev/malcontent/commit/356c56659ccfcad0b249a97de8cf71f151ed3ee9ghsax_refsource_MISCWEB
- github.com/chainguard-dev/malcontent/pull/1383ghsax_refsource_MISCWEB
- github.com/chainguard-dev/malcontent/security/advisories/GHSA-945p-3jhm-6rcpghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.