malcontent's archive extraction could write outside extraction directory
Description
malcontent discovers supply-chain compromises through. context, differential analysis, and YARA. Starting in version 1.8.0 and prior to version 1.20.3, malcontent could be made to create symlinks outside the intended extraction directory when scanning a specially crafted tar or deb archive. The handleSymlink function received arguments in the wrong order, causing the symlink target to be used as the symlink location. Additionally, symlink targets were not validated to ensure they resolved within the extraction directory. Version 1.20.3 introduces fixes that swap handleSymlink arguments, validate symlink location, and validate symlink targets that resolve within an extraction directory.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/chainguard-dev/malcontentGo | >= 1.8.0, < 1.20.3 | 1.20.3 |
Affected products
1- Range: v1.10.0, v1.10.1, v1.10.2, …
Patches
2259fca5abc00Merge commit from fork
2 files changed · +40 −11
pkg/archive/archive.go+11 −0 modified@@ -316,6 +316,17 @@ func handleSymlink(dir, linkPath, linkTarget string) error { return fmt.Errorf("symlink location outside extraction directory: %s", fullPath) } + // Skip absolute symlink targets + if filepath.IsAbs(linkTarget) { + return nil + } + + // Validate relative symlink target resolves within extraction directory + resolvedTarget := filepath.Clean(filepath.Join(filepath.Dir(fullPath), linkTarget)) + if !IsValidPath(resolvedTarget, dir) { + return fmt.Errorf("symlink target escapes extraction directory: %s -> %s", linkPath, linkTarget) + } + // Remove existing symlinks if _, err := os.Lstat(fullPath); err == nil { if err := os.Remove(fullPath); err != nil {
pkg/archive/symlink_test.go+29 −11 modified@@ -14,11 +14,10 @@ func TestSymlinkExtraction(t *testing.T) { wantErr bool }{ { - // Symlinks pointing outside the directory are allowed - // (common in container images, e.g., /etc/mtab -> /proc/mounts) - name: "symlink target outside directory is allowed", + // Relative symlink that escapes the extraction directory should be rejected + name: "relative symlink escaping directory is rejected", tarFile: "testdata/symlink_escape.tar", - wantErr: false, + wantErr: true, }, { name: "valid symlink within directory", @@ -52,27 +51,46 @@ func TestSymlinkExtraction(t *testing.T) { } func TestHandleSymlink(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "symlink-location-test-*") + tmpDir, err := os.MkdirTemp("", "symlink-test-*") if err != nil { t.Fatalf("failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) - // Symlink location that would escape should be rejected + // A symlink location which escapes should be rejected err = handleSymlink(tmpDir, "../escape", "target") if err == nil { t.Error("expected error for symlink location escaping directory") } - // Valid symlink location should succeed - err = handleSymlink(tmpDir, "valid_link", "/some/absolute/target") + // Absolute symlink targets are skipped (no error, no symlink created) + err = handleSymlink(tmpDir, "abs_link", "/some/absolute/target") if err != nil { - t.Errorf("unexpected error for valid symlink location: %v", err) + t.Errorf("unexpected error for absolute symlink target: %v", err) + } + if _, err := os.Lstat(filepath.Join(tmpDir, "abs_link")); err == nil { + t.Error("absolute symlink should not have been created") + } + + // A relative symlink target which escapes should be rejected + err = handleSymlink(tmpDir, "escape_link", "../../etc/passwd") + if err == nil { + t.Error("expected error for relative symlink target escaping directory") } - // Verify symlink was created + // Write a file we can create a valid symlink for + targetFile := filepath.Join(tmpDir, "realfile.txt") + if err := os.WriteFile(targetFile, []byte("test"), 0o644); err != nil { + t.Fatalf("failed to create target file: %v", err) + } + + // A valid relative symlink should succeed + err = handleSymlink(tmpDir, "valid_link", "realfile.txt") + if err != nil { + t.Errorf("unexpected error for valid symlink: %v", err) + } linkPath := filepath.Join(tmpDir, "valid_link") if _, err := os.Lstat(linkPath); err != nil { - t.Errorf("symlink was not created: %v", err) + t.Errorf("valid symlink was not created: %v", err) } }
a7dd8a5328ddMerge commit from fork
6 files changed · +91 −22
pkg/archive/archive.go+11 −20 modified@@ -306,35 +306,26 @@ func handleFile(target string, tr *tar.Reader) error { } // handleSymlink creates valid symlinks when extracting .deb or .tar archives. -func handleSymlink(dir, linkName, target string) error { - // Skip symlinks for targets that do not exist - _, err := os.Readlink(target) - if os.IsNotExist(err) { - return nil - } +// linkPath is where the symlink will be created (relative to dir). +// linkTarget is what the symlink points to. +func handleSymlink(dir, linkPath, linkTarget string) error { + fullPath := filepath.Join(dir, linkPath) - fullLink := filepath.Join(dir, linkName) + // Validate symlink location is within extraction directory + if !IsValidPath(fullPath, dir) { + return fmt.Errorf("symlink location outside extraction directory: %s", fullPath) + } // Remove existing symlinks - if _, err := os.Lstat(fullLink); err == nil { - if err := os.Remove(fullLink); err != nil { + if _, err := os.Lstat(fullPath); err == nil { + if err := os.Remove(fullPath); err != nil { return fmt.Errorf("failed to remove existing symlink: %w", err) } } - if err := os.Symlink(target, fullLink); err != nil { + if err := os.Symlink(linkTarget, fullPath); err != nil { return fmt.Errorf("failed to create symlink: %w", err) } - linkReal, err := filepath.EvalSymlinks(fullLink) - if err != nil { - os.Remove(fullLink) - return fmt.Errorf("failed to evaluate symlink: %w", err) - } - if !IsValidPath(linkReal, dir) { - os.Remove(fullLink) - return fmt.Errorf("symlink points outside temporary directory: %s", linkReal) - } - return nil }
pkg/archive/deb.go+1 −1 modified@@ -73,7 +73,7 @@ func ExtractDeb(ctx context.Context, d, f string) error { return fmt.Errorf("failed to extract file: %w", err) } case tar.TypeSymlink: - if err := handleSymlink(d, header.Linkname, target); err != nil { + if err := handleSymlink(d, clean, header.Linkname); err != nil { return fmt.Errorf("failed to create symlink: %w", err) } }
pkg/archive/symlink_test.go+78 −0 added@@ -0,0 +1,78 @@ +package archive + +import ( + "context" + "os" + "path/filepath" + "testing" +) + +func TestSymlinkExtraction(t *testing.T) { + tests := []struct { + name string + tarFile string + wantErr bool + }{ + { + // Symlinks pointing outside the directory are allowed + // (common in container images, e.g., /etc/mtab -> /proc/mounts) + name: "symlink target outside directory is allowed", + tarFile: "testdata/symlink_escape.tar", + wantErr: false, + }, + { + name: "valid symlink within directory", + tarFile: "testdata/symlink_valid.tar", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "symlink-test-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + ctx := context.Background() + err = ExtractTar(ctx, tmpDir, tt.tarFile) + + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + } + }) + } +} + +func TestHandleSymlink(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "symlink-location-test-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Symlink location that would escape should be rejected + err = handleSymlink(tmpDir, "../escape", "target") + if err == nil { + t.Error("expected error for symlink location escaping directory") + } + + // Valid symlink location should succeed + err = handleSymlink(tmpDir, "valid_link", "/some/absolute/target") + if err != nil { + t.Errorf("unexpected error for valid symlink location: %v", err) + } + + // Verify symlink was created + linkPath := filepath.Join(tmpDir, "valid_link") + if _, err := os.Lstat(linkPath); err != nil { + t.Errorf("symlink was not created: %v", err) + } +}
pkg/archive/tar.go+1 −1 modified@@ -207,7 +207,7 @@ func ExtractTar(ctx context.Context, d string, f string) error { return fmt.Errorf("failed to extract file: %w", err) } case tar.TypeSymlink: - if err := handleSymlink(d, header.Linkname, target); err != nil { + if err := handleSymlink(d, clean, header.Linkname); err != nil { return fmt.Errorf("failed to create symlink: %w", err) } }
pkg/archive/testdata/symlink_escape.tar+0 −0 addedpkg/archive/testdata/symlink_valid.tar+0 −0 added
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-923j-vrcg-hxwhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-24846ghsaADVISORY
- github.com/chainguard-dev/malcontent/commit/259fca5abc004f3ab238895463ef280a87f30e96ghsax_refsource_MISCWEB
- github.com/chainguard-dev/malcontent/commit/a7dd8a5328ddbaf235568437813efa7591e00017ghsax_refsource_MISCWEB
- github.com/chainguard-dev/malcontent/security/advisories/GHSA-923j-vrcg-hxwhghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.