
Proof-of-concept demonstrating a path traversal vulnerability (CVE-2026-35204) in Helm plugin installation, allowing arbitrary file write via crafted plugin metadata.
This repository represents a PoC to a CVE-2026-35204
"Helm is a package manager for Charts for Kubernetes. From 4.0.0 to 4.1.3, a specially crafted Helm plugin, when installed or updated, will cause Helm to write the contents of the plugin to an arbitrary filesystem location. To prevent this, validate that the plugin.yaml of the Helm plugin does not include a version: field containing POSIX dot-dot path separators ie. "/../". This vulnerability is fixed in 4.1.4."
This PoC demonstrates that metadata.Version is used to construct the path of plugin archive, allowing path traversal and writing the downloaded plugin.tgz outside the intended plugin directory. The plugin contents are still extracted into the designated cache directory before being installed, so this PoC does not demonstrate arbitrary extraction or arbitrary host file overwrite.
Helm plugin installer has an interface with four implementations (HTTP, local, VCS and OCI). This PoC focuses on the HTTP installer. Unlike the HTTP/OCI installers, the VCS installer does not construct a tarball path from metadata.Version at all. Here is a code block, that prepares the tarballPath:
filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
tarballPath := helmpath.DataPath("plugins", filename)
where metadata.Version is the variable we control in plugin.yaml. To trigger the vulnerability, you need to package your crafted plugin into .tgz archive, since its metadata.Version is the one being extracted by plugin.ExtractTgzPluginMetadata and downloaded via HTTP/OCI/local installer. Therefore, to achieve path traversal, we can use http_installer, which fetches plugins as an archive served by a web server.
In the example below, helm binary was manually rebuilt to enable logging messages, where you can observe two different data flows. First is plugin installation with correct version name. Second one is plugin installation that has dot-dot path separators in its version name:
./helm plugin install http://0.0.0.0:8000/evil.tar.gz
Verifying plugin signature...
WARNING: No provenance file found for plugin. Plugin is not signed and cannot be verified.
filename -> evil-2.2.2.tgz
tarballPath -> /home/s0m3body/.local/share/helm/plugins/evil-2.2.2.tgz
i.CacheDir -> /home/s0m3body/.cache/helm/plugins/http-0.0.0.0-8000-evil.tar.gz
i.Path() -> /home/s0m3body/.local/share/helm/plugins/evil
Installed plugin: evil
./helm plugin install http://127.0.0.1:8000/evil.tar.gz
Verifying plugin signature...
WARNING: No provenance file found for plugin. Plugin is not signed and cannot be verified.
filename -> evil-../../../../../../.ssh/pwn3d.tgz
tarballPath -> /home/s0m3body/.ssh/pwn3d.tgz
i.CacheDir -> /home/s0m3body/.cache/helm/plugins/http-127.0.0.1-8000-evil.tar.gz
i.Path() -> /home/s0m3body/.local/share/helm/plugins/evil
Installed plugin: evil
The runtime traces demonstrate that the path traversal affects only the location where the downloaded plugin archive is placed, which is basically the HELM_PLUGINS env variable. The cache (i.CacheDir) and the final extraction (i.Path()) remain unchanged. With that in mind, we can manipulate the temporary archive destination and its name resulting in a file write.
You do not need to run anything or use post install webhooks, simple plugin installation is enough to trigger the vulnerability.
During my analysis of the HTTP installer, I observed that archive extraction occurs into i.CacheDir, while installation (fs.CopyDir) copies files from i.CacheDir into i.Path(). I did not find a code path where the traversal-influenced tarballPath affects archive content. Therfore, based on the analyzed runtime tracing, the archive extraction destination is independent of the traversal-controlled tarballPath.
The advisory mentions both installation and update. For the HTTP/local/VCS installers, Update() is not implemented; the OCI installer implements update by invoking Install() after removing the previous plugin.
Here is the Update() method from HTTP installer:
func (i *HTTPInstaller) Update() error {
return fmt.Errorf("method Update() not implemented for HttpInstaller")
}
To mitigate the vulnerability, the isValidSemverfunction was added, which is now called when Validate() inside metadata.go is invoked.
"github.com/Masterminds/semver/v3"
func isValidSemver(v string) bool {
_, err := semver.NewVersion(v)
return err == nil
}
if m.Version != "" && !isValidSemver(m.Version) {
errs = append(errs, fmt.Errorf("invalid plugin version %q: must be valid semver", m.Version))
}
As you can see, if the version is not a valid semver (and ../../../../ is not), then we will receive an error in the process of evil helm plugin installation.
To run the PoC you can use scripts/pos.sh script:
./scripts/poc.sh
Which will result in something like that:

MIT