
Preuve de concept démontrant une vulnérabilité de traversée de chemin (CVE-2026-35204) dans l'installation de plugins Helm, permettant l'écriture arbitraire de fichiers via des métadonnées de plugin malveillantes.
Ce dépôt constitue une PoC pour la CVE-2026-35204
« Helm est un gestionnaire de paquets pour les Charts Kubernetes. De 4.0.0 à 4.1.3, un plugin Helm spécialement conçu, lorsqu'il est installé ou mis à jour, amène Helm à écrire le contenu du plugin à un emplacement arbitraire du système de fichiers. Pour éviter cela, validez que le plugin.yaml du plugin Helm ne contient pas de champ version: incluant des séparateurs de chemin POSIX point-point, c'est-à-dire "/../". Cette vulnérabilité est corrigée dans 4.1.4. »
Cette PoC démontre que metadata.Version est utilisé pour construire le chemin de l'archive du plugin, permettant une traversée de chemin et l'écriture du plugin.tgz téléchargé en dehors du répertoire de plugin prévu. Le contenu du plugin est toutefois extrait dans le répertoire de cache désigné avant d'être installé, donc cette PoC ne démontre pas d'extraction arbitraire ni d'écrasement arbitraire de fichiers de l'hôte.
La vulnérabilité est classifiée comme CWE-22 - Limitation incorrecte d'un nom de chemin à un répertoire restreint (« Path Traversal »)
L'installateur de plugins Helm possède une interface avec quatre implémentations (HTTP, local, VCS et OCI). Cette PoC se concentre sur l'installateur HTTP. Contrairement aux installateurs HTTP/OCI, l'installateur VCS ne construit pas du tout de chemin de tarball à partir de metadata.Version. Voici un bloc de code qui prépare le tarballPath :
filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
tarballPath := helmpath.DataPath("plugins", filename)
où metadata.Version est la variable que nous contrôlons dans plugin.yaml. Pour déclencher la vulnérabilité, vous devez empaqueter votre plugin conçu dans une archive .tgz, puisque c'est son metadata.Version qui est extrait par plugin.ExtractTgzPluginMetadata et téléchargé via l'installateur HTTP/OCI/local. Par conséquent, pour réaliser la traversée de chemin, nous pouvons utiliser http_installer, qui récupère les plugins sous forme d'archive servie par un serveur web.
Dans l'exemple ci-dessous, le binaire helm a été reconstruit manuellement pour activer les messages de journalisation, où vous pouvez observer deux flux de données différents. Le premier est l'installation du plugin avec un nom de version correct. Le second est l'installation du plugin dont le nom de version contient des séparateurs de chemin point-point :
./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
Les traces d'exécution démontrent que la traversée de chemin n'affecte que l'emplacement où l'archive du plugin téléchargée est placée, ce qui correspond essentiellement à la variable d'environnement HELM_PLUGINS. Le cache (i.CacheDir) et l'extraction finale (i.Path()) restent inchangés. Avec cela en tête, nous pouvons manipuler la destination temporaire de l'archive et son nom, ce qui entraîne une écriture de fichier.
Vous n'avez pas besoin d'exécuter quoi que ce soit ni d'utiliser des webhooks post-installation ; une simple installation de plugin suffit à déclencher la vulnérabilité.
Lors de mon analyse de l'installateur HTTP, j'ai observé que l'extraction de l'archive se produit dans i.CacheDir, tandis que l'installation (fs.CopyDir) copie les fichiers de i.CacheDir vers i.Path(). Je n'ai pas trouvé de chemin de code où le tarballPath influencé par la traversée affecte le contenu de l'archive. Par conséquent, d'après la trace d'exécution analysée, la destination d'extraction de l'archive est indépendante du tarballPath contrôlé par la traversée.
L'avis mentionne à la fois l'installation et la mise à jour. Pour les installateurs HTTP/local/VCS, Update() n'est pas implémenté ; l'installateur OCI implémente la mise à jour en invoquant Install() après avoir supprimé le plugin précédent.
Voici la méthode Update() de l'installateur HTTP :
func (i *HTTPInstaller) Update() error {
return fmt.Errorf("method Update() not implemented for HttpInstaller")
}
Pour atténuer la vulnérabilité, la fonction isValidSemver a été ajoutée, et est désormais appelée lorsque Validate() dans metadata.go est invoquée.
"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))
}
Comme vous pouvez le voir, si la version n'est pas un semver valide (et ../../../../ ne l'est pas), alors nous recevrons une erreur lors du processus d'installation du plugin helm malveillant.
Pour exécuter la PoC, vous pouvez utiliser le script scripts/pos.sh :
./scripts/poc.sh
Ce qui produira quelque chose comme ceci :

MIT