Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions internal/plugins/helm/v1/chartutil/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,38 @@ func fetchChartDependencies(chartPath string) error {
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
}
// CVE-2025-53547: Ensure Chart.lock is not a symlink before dependency build to prevent code injection
if err := validateForSymlink(chartPath); err != nil {
return err
}

if err := man.Build(); err != nil {
fmt.Println(out.String())
return err
}
return nil
}

// validateForSymlink checks if Chart.lock is a symbolic link to prevent CVE-2025-53547.
//
// Reference: https://github.com/helm/helm/security/advisories/GHSA-557j-xg8c-q2mm
//
// Note: The upstream fix requires a higher Go version than is currently available
// downstream. Therefore, we are using this wrapper to avoid upgrading Helm or Go.
func validateForSymlink(chartPath string) error {
dest := filepath.Join(chartPath, "Chart.lock")

info, err := os.Lstat(dest)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error getting info for %q: %w", dest, err)
} else if err == nil {
if info.Mode()&os.ModeSymlink != 0 {
link, err := os.Readlink(dest)
if err != nil {
return fmt.Errorf("error reading symlink for %q: %w", dest, err)
}
return fmt.Errorf("the Chart.lock file is a symlink to %q. This could lead to code injection (CVE-2025-53547)", link)
}
}
return nil
}
131 changes: 131 additions & 0 deletions internal/plugins/helm/v1/chartutil/chart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,134 @@ func runTestCase(t *testing.T, testDir string, tc createChartTestCase) {
assert.NoError(t, err)
assert.Equal(t, filepath.Join(chartutil.HelmChartsDir, tc.expectChartName), chartPath)
}

// TestCVE2025_53547_Protection tests that ScaffoldChart properly detects and blocks symlinked Chart.lock files.
// This validates the CVE-2025-53547 mitigation implemented in validateForSymlink function.
// Reference: https://github.com/helm/helm/security/advisories/GHSA-557j-xg8c-q2mm
func TestCVE2025_53547_Protection(t *testing.T) {
// Set up test server like in TestChart
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz")
if err != nil {
t.Fatalf("Failed to create new temp server: %s", err)
}
defer srv.Stop()

if err := srv.LinkIndices(); err != nil {
t.Fatalf("Failed to link server indices: %s", err)
}

tests := []struct {
name string
setupAfterScaffolding func(string) error // Setup function called after initial scaffolding
expectCVEError bool
}{
{
name: "safe chart with no Chart.lock",
setupAfterScaffolding: func(scaffoldedChartPath string) error {
// Remove any Chart.lock file
chartLockPath := filepath.Join(scaffoldedChartPath, "Chart.lock")
os.Remove(chartLockPath)
return nil
},
expectCVEError: false,
},
{
name: "safe chart with regular Chart.lock",
setupAfterScaffolding: func(scaffoldedChartPath string) error {
// Ensure a regular (non-symlinked) Chart.lock file exists
chartLockPath := filepath.Join(scaffoldedChartPath, "Chart.lock")

if _, err := os.Stat(chartLockPath); os.IsNotExist(err) {
return fmt.Errorf("chart.lock doesn't exist at %s: %v", chartLockPath, err)
} else if err != nil {
return err
}
return nil
},
expectCVEError: false,
},
{
name: "CVE-2025-53547: symlinked Chart.lock attack",
setupAfterScaffolding: func(scaffoldedChartPath string) error {
// Create target file for symlink
targetFile := filepath.Join(scaffoldedChartPath, "target.yaml")
targetContent := "dependencies: []"
if err := os.WriteFile(targetFile, []byte(targetContent), 0644); err != nil {
return err
}

// Remove any existing Chart.lock and create symlink
chartLockPath := filepath.Join(scaffoldedChartPath, "Chart.lock")
os.Remove(chartLockPath) // Remove if exists
return os.Symlink("target.yaml", chartLockPath)
},
expectCVEError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set up Helm environment like in runTestCase
os.Setenv("XDG_CONFIG_HOME", filepath.Join(srv.Root(), ".config"))
os.Setenv("XDG_CACHE_HOME", filepath.Join(srv.Root(), ".cache"))
os.Setenv("HELM_REPOSITORY_CONFIG", filepath.Join(srv.Root(), "repositories.yaml"))
os.Setenv("HELM_REPOSITORY_CACHE", filepath.Join(srv.Root()))
defer os.Unsetenv("XDG_CONFIG_HOME")
defer os.Unsetenv("XDG_CACHE_HOME")
defer os.Unsetenv("HELM_REPOSITORY_CONFIG")
defer os.Unsetenv("HELM_REPOSITORY_CACHE")

// Create temporary directories
tmpProjectDir, err := os.MkdirTemp("", "test-project-cve")
assert.NoError(t, err)
defer os.RemoveAll(tmpProjectDir)

tmpChartDir, err := os.MkdirTemp("", "test-chart-cve")
assert.NoError(t, err)
defer os.RemoveAll(tmpChartDir)

// Step 1: Create chart with dependencies using test server
chartYaml := fmt.Sprintf(`apiVersion: v2
name: test-chart
version: 1.0.0
appVersion: 1.0.0
description: Test chart for CVE-2025-53547 testing
type: application
dependencies:
- name: test-chart
version: "1.2.3"
repository: "%s"
`, srv.URL())
err = os.WriteFile(filepath.Join(tmpChartDir, "Chart.yaml"), []byte(chartYaml), 0644)
assert.NoError(t, err)

// Step 2: Initial scaffolding
chrt, err := chartutil.LoadChart(chartutil.Options{Chart: tmpChartDir})
assert.NoError(t, err)
assert.NotNil(t, chrt)

_, _, err = chartutil.ScaffoldChart(chrt, tmpProjectDir)
assert.NoError(t, err)

// Step 3: Setup test scenario in scaffolded chart
scaffoldedChartPath := filepath.Join(tmpProjectDir, chartutil.HelmChartsDir, chrt.Name())
err = tt.setupAfterScaffolding(scaffoldedChartPath)
assert.NoError(t, err)

// Step 4: Try to scaffold again
chrt2, err := chartutil.LoadChart(chartutil.Options{Chart: scaffoldedChartPath})
assert.NoError(t, err)
assert.NotNil(t, chrt2)

_, _, err = chartutil.ScaffoldChart(chrt2, tmpProjectDir)

if tt.expectCVEError {
// CVE attack case: should fail with CVE protection message
assert.Error(t, err)
assert.Contains(t, err.Error(), "the Chart.lock file is a symlink to")
} else {
assert.NoError(t, err)
}
})
}
}