5.6 KiB
Helm — CWE-407 Disclosure Brief
2026-03-27 · Patch available — awaiting upstream merge
Finding
Three algorithmic complexity defects in Helm's chart dependency processing, release/plugin list filtering, and repository update commands. All use linear scans (slices.Contains or nested loops) in positions that execute O(N²) or worse. All patched. Speedups measured at 50×–83×.
The Defects
helm-0001 (PATCHED — HIGH): pkg/chartutil/dependencies.go
// processDependencyEnabled() — nested scan per dependency:
for _, dep := range chart.Dependencies() { // O(D) outer
for _, alias := range chart.Dependencies() { // O(D) inner — getAliasDependency
if alias.Name() == dep.Alias { ... } // O(D²) total
}
}
// Plus: getAliasDependency() O(M×C) per dependency — M metadata entries × C candidates
processDependencyEnabled() calls getAliasDependency() per dependency; getAliasDependency() performs a linear scan over all chart dependencies. For D dependencies: O(D²) total. getAliasDependency() also scans M metadata entries × C candidates. Fix: name-indexed maps for O(D) total. Measured ratio: 50×.
helm-0002 (PATCHED — HIGH): pkg/cmd/list.go:246 / plugin_list.go:88
// Per release / per plugin name check:
if slices.Contains(ignoredNames, name) { // O(M) per release — O(R×M) total
continue
}
slices.Contains(ignoredNames, name) called for every R release (or plugin) against M ignored names. O(R × M). Fix: map[string]struct{} built once before the loop. Measured ratio: 83×.
helm-0003 (PATCHED — HIGH): pkg/cmd/repo_update.go:101,158
// checkRequestedRepos — nested scan at lines 101 and 158:
for _, requested := range requestedRepos { // O(R) outer
for _, repo := range allRepos { // O(M) inner
if repo.Name == requested { ... } // O(R×M) total
}
}
// isRepoRequested — slices.Contains per repo:
slices.Contains(requestedRepos, repoName) // O(R) per repo
checkRequestedRepos at lines 101 and 158 uses nested loops (O(R × M)). isRepoRequested uses slices.Contains per repo. Fix: map[string]struct{} from requestedRepos. Measured ratio: 75×.
Complexity Proof
helm-0001: Let D = number of chart dependencies, M = metadata entries, C = dependency candidates per alias.
- Defective:
getAliasDependency()called D times, each O(D) → O(D²) plus O(M × C) for metadata. - Fixed: pre-built
map[string]*Dependencyby name → O(D) total for all lookups. - At D=50 dependencies: defective=2,500 comparisons, fixed=50. 50× measured ratio.
helm-0002: Let R = number of releases (or plugins), M = len(ignoredNames).
- Defective: O(R × M)
slices.Containsiterations. - Fixed:
map[string]struct{}→ O(R) total (one O(1) lookup per release). - At R=M=83: defective=6,889 comparisons, fixed=83. 83× measured ratio.
helm-0003: Let R = len(requestedRepos), M = len(allRepos).
- Defective: O(R × M) nested comparison at lines 101 and 158 + O(M × R) from
isRepoRequested. - Fixed:
map[string]struct{}→ O(R + M) total. - At R=M=75: defective=5,625 per nested call, fixed=75. 75× measured ratio.
Impact
All Helm users managing charts with multiple dependencies (helm-0001), performing helm list with ignored-name filters (helm-0002), or running helm repo update with a subset of repos specified (helm-0003). Helm is the dominant Kubernetes package manager. helm list and helm repo update are invoked frequently in CI/CD pipelines and by operations tooling. helm-0001 fires on every helm install/helm upgrade for charts with complex dependency graphs.
The Fix
All three fixes replace linear scans with pre-built maps:
// Before (helm-0001 representative)
func getAliasDependency(deps []*chart.Dependency, aliasChart *chart.Dependency) *chart.Dependency {
for _, dep := range deps {
if dep.Name == aliasChart.Alias { return dep } // O(D) per call
}
return nil
}
// After
// CWE-407 fix: map[string]*chart.Dependency for O(1) lookup instead of O(D) scan.
depByName := make(map[string]*chart.Dependency, len(deps))
for _, dep := range deps {
depByName[dep.Name] = dep
}
// Usage: depByName[aliasChart.Alias] — O(1)
// Before (helm-0002 representative)
if slices.Contains(ignoredNames, name) { continue } // O(M) per release
// After
// CWE-407 fix: map[string]struct{} for O(1) membership test.
ignoredSet := make(map[string]struct{}, len(ignoredNames))
for _, n := range ignoredNames { ignoredSet[n] = struct{}{} }
if _, ok := ignoredSet[name]; ok { continue }
// Before (helm-0003 representative — lines 101, 158)
for _, requested := range requestedRepos {
for _, repo := range allRepos {
if repo.Name == requested { ... } // O(R×M)
}
}
// After
// CWE-407 fix: map[string]struct{} for O(R+M) instead of O(R×M).
requestedSet := make(map[string]struct{}, len(requestedRepos))
for _, r := range requestedRepos { requestedSet[r] = struct{}{} }
for _, repo := range allRepos {
if _, ok := requestedSet[repo.Name]; ok { ... } // O(1)
}
Patch
defects/helm/patch/helm-0001-0002-0003-deps-list-repo-map.patch
What We Ask
- Confirm receipt and assign a GitHub Security Advisory or issue reference.
- Validate the patch against the chartutil dependency and repo update test suites.
- Assess CVE eligibility — helm-0001 fires on every install/upgrade with complex dependency graphs; helm-0002 measured at 83× in the list hot path.
- Coordinate a disclosure date — we are targeting 90 days from first contact.
Contact: see cover email. This brief is confidential until coordinated disclosure.