melange has a path traversal in license-path which allows reading files outside workspace
Description
melange allows users to build apk packages using declarative pipelines. From version 0.14.0 to before 0.40.3, an attacker who can influence a melange configuration file (e.g., through pull request-driven CI or build-as-a-service scenarios) could read arbitrary files from the host system. The LicensingInfos function in pkg/config/config.go reads license files specified in copyright[].license-path without validating that paths remain within the workspace directory, allowing path traversal via ../ sequences. The contents of the traversed file are embedded into the generated SBOM as license text, enabling exfiltration of sensitive data through build artifacts. This issue has been patched in version 0.40.3.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2026-25145: a path traversal in melange's LicensingInfos allows an attacker controlling the config file to read arbitrary host files and embed them in SBOMs, fixed in v0.40.3.
Vulnerability
The LicensingInfos function in pkg/config/config.go reads license files specified by the copyright[].license-path field in melange configuration files without validating that the path remains within the workspace directory. This allows an attacker to use ../ sequences to traverse directories and read arbitrary files from the host filesystem. [1][3][4]
Exploitation
An attacker who can influence a melange configuration file—for example, through pull request-driven CI pipelines or a build-as-a-service scenario—can craft a configuration with a malicious license-path pointing to a sensitive file (e.g., /etc/shadow or credentials). The function will read that file and include its contents in the generated Software Bill of Materials (SBOM), which is typically published as a build artifact. [3][4]
Impact
The impact is information disclosure: sensitive data from the host system is exfiltrated through the SBOM. No authentication beyond the ability to submit a configuration is required; the attacker does not need to execute code, as configuration injection is sufficient. [3][4]
Mitigation
The issue has been patched in melange version 0.40.3. The fix adds validation to ensure license-path resolves within the workspace directory, as demonstrated in the test added by commit 2f95c9f. Users should upgrade immediately. [1][3][4]
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
chainguard.dev/melangeGo | >= 0.14.0, < 0.40.3 | 0.40.3 |
Affected products
2- Range: >=0.14.0 <0.40.3
- chainguard-dev/melangev5Range: >= 0.14.0, < 0.40.3
Patches
12f95c9f4355eMerge commit from fork
2 files changed · +170 −2
pkg/config/config.go+9 −2 modified@@ -529,8 +529,15 @@ func (p Package) LicensingInfos(ctx context.Context, workspaceDir string) (map[s id := normalizeLicenseID(license) if cp.LicensePath != "" { - // Read license content from file - content, err := os.ReadFile(filepath.Join(workspaceDir, cp.LicensePath)) // #nosec G304 - Reading license file from build workspace + // Clean and localize the path + cleanPath := filepath.Clean(cp.LicensePath) + localPath, err := filepath.Localize(cleanPath) + if err != nil { + return nil, fmt.Errorf("invalid license-path %q: %w", cp.LicensePath, err) + } + fullPath := filepath.Join(workspaceDir, localPath) + + content, err := os.ReadFile(fullPath) // #nosec G304 - Reading license file from build workspace if err != nil { return nil, fmt.Errorf("failed to read licensepath %q: %w", cp.LicensePath, err) }
pkg/config/config_test.go+161 −0 modified@@ -2145,3 +2145,164 @@ func TestLicensingInfosWithValidation(t *testing.T) { }) } } + +// TestLicensingInfosPathTraversal tests that LicensingInfos properly validates +// license-path +func TestLicensingInfosPathTraversal(t *testing.T) { + ctx := slogtest.Context(t) + + // Create a temporary directory structure simulating a workspace with files outside it + tmpRoot := t.TempDir() + workspaceDir := filepath.Join(tmpRoot, "workspace") + require.NoError(t, os.MkdirAll(workspaceDir, 0o755)) + + // Create a legitimate license file inside workspace + legitimateLicense := "This is a legitimate license file" + require.NoError(t, os.WriteFile(filepath.Join(workspaceDir, "LICENSE"), []byte(legitimateLicense), 0o644)) + + // Create sensitive files + sensitiveDir := filepath.Join(tmpRoot, "sensitive") + require.NoError(t, os.MkdirAll(sensitiveDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(sensitiveDir, "secrets.txt"), []byte("SENSITIVE"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(tmpRoot, "credentials.env"), []byte("CREDS"), 0o644)) + + tests := []struct { + name string + copyright []Copyright + expectError bool + description string + }{ + { + name: "legitimate relative path within workspace", + copyright: []Copyright{ + {License: "MIT", LicensePath: "LICENSE"}, + }, + expectError: false, + description: "A normal license file path should work", + }, + { + name: "simple parent directory traversal", + copyright: []Copyright{ + {License: "CustomLicense", LicensePath: "../sensitive/secrets.txt"}, + }, + expectError: true, + description: "Simple ../ should be blocked to prevent reading files outside workspace", + }, + { + name: "multiple parent directory traversal", + copyright: []Copyright{ + {License: "CustomLicense", LicensePath: "../../credentials.env"}, + }, + expectError: true, + description: "Multiple ../ levels should be blocked", + }, + { + name: "traversal with valid path prefix", + copyright: []Copyright{ + {License: "CustomLicense", LicensePath: "subdir/../../sensitive/secrets.txt"}, + }, + expectError: true, + description: "Paths that go up and then outside should be blocked", + }, + { + name: "absolute path", + copyright: []Copyright{ + {License: "CustomLicense", LicensePath: "/etc/passwd"}, + }, + expectError: true, + description: "Absolute paths should be rejected", + }, + { + name: "absolute path to sensitive file", + copyright: []Copyright{ + {License: "CustomLicense", LicensePath: filepath.Join(sensitiveDir, "secrets.txt")}, + }, + expectError: true, + description: "Absolute paths to any file should be rejected", + }, + { + name: "dot-dot in middle of path escaping", + copyright: []Copyright{ + {License: "CustomLicense", LicensePath: "foo/../../../sensitive/secrets.txt"}, + }, + expectError: true, + description: "Complex paths with .. that escape should be blocked", + }, + { + name: "relative path with ./ prefix", + copyright: []Copyright{ + {License: "MIT", LicensePath: "./LICENSE"}, + }, + expectError: false, + description: "Relative paths with ./ prefix within workspace should work", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkg := Package{ + Copyright: tt.copyright, + } + + result, err := pkg.LicensingInfos(ctx, workspaceDir) + + if tt.expectError { + require.Error(t, err, "Expected an error but got none for: %s", tt.description) + } else { + require.NoError(t, err, "Expected no error but got: %v for: %s", err, tt.description) + + // For successful cases, verify the content if a file was read + if tt.copyright[0].LicensePath != "" { + id := normalizeLicenseID(tt.copyright[0].License) + content, exists := result[id] + require.True(t, exists, "Expected license info to be present") + require.Equal(t, legitimateLicense, content, "Content should match legitimate license") + } + } + }) + } +} + +// TestLicensingInfosMultipleLicenses tests that one malicious path fails the entire operation +func TestLicensingInfosMultipleLicenses(t *testing.T) { + ctx := slogtest.Context(t) + + tmpRoot := t.TempDir() + workspaceDir := filepath.Join(tmpRoot, "workspace") + require.NoError(t, os.MkdirAll(workspaceDir, 0o755)) + + goodLicense := "This is a good license" + require.NoError(t, os.WriteFile(filepath.Join(workspaceDir, "LICENSE"), []byte(goodLicense), 0o644)) + + // Create sensitive file outside workspace + require.NoError(t, os.WriteFile(filepath.Join(tmpRoot, "secret"), []byte("SENSITIVE"), 0o644)) + + t.Run("one bad path in multiple licenses should fail entire operation", func(t *testing.T) { + pkg := Package{ + Copyright: []Copyright{ + {License: "MIT", LicensePath: "LICENSE"}, + {License: "Apache-2.0", LicensePath: "../../secret"}, + {License: "BSD-3-Clause", LicensePath: "LICENSE"}, + }, + } + + _, err := pkg.LicensingInfos(ctx, workspaceDir) + require.Error(t, err, "Should fail when any license path is malicious") + }) + + t.Run("all good paths should succeed", func(t *testing.T) { + pkg := Package{ + Copyright: []Copyright{ + {License: "MIT", LicensePath: "LICENSE"}, + {License: "Apache-2.0", LicensePath: "LICENSE"}, + {License: "BSD-3-Clause", LicensePath: ""}, + }, + } + + result, err := pkg.LicensingInfos(ctx, workspaceDir) + require.NoError(t, err) + require.Len(t, result, 2) + require.Equal(t, goodLicense, result["MIT"]) + require.Equal(t, goodLicense, result["Apache-2.0"]) + }) +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-2w4f-9fgg-q2v9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-25145ghsaADVISORY
- github.com/chainguard-dev/melange/commit/2f95c9f4355ed993f2670bf1bb82d88b0f65e9e4ghsax_refsource_MISCWEB
- github.com/chainguard-dev/melange/security/advisories/GHSA-2w4f-9fgg-q2v9ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.