VYPR
Moderate severityNVD Advisory· Published Feb 4, 2026· Updated Feb 5, 2026

melange has a path traversal in license-path which allows reading files outside workspace

CVE-2026-25145

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.

PackageAffected versionsPatched versions
chainguard.dev/melangeGo
>= 0.14.0, < 0.40.30.40.3

Affected products

2

Patches

1
2f95c9f4355e

Merge commit from fork

https://github.com/chainguard-dev/melangeŁukasz ZemczakJan 29, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.