
Proof-of-concept, демонстрирующий уязвимость path traversal (CVE-2026-35204) при установке плагинов Helm, позволяющую запись произвольных файлов через специально созданные метаданные плагина.
Этот репозиторий представляет собой PoC для CVE-2026-35204
"Helm — это менеджер пакетов для Charts в Kubernetes. В версиях от 4.0.0 до 4.1.3 специально созданный плагин Helm при установке или обновлении заставляет Helm записывать содержимое плагина в произвольное место файловой системы. Чтобы предотвратить это, проверьте, что plugin.yaml плагина Helm не содержит поля version: с разделителями пути POSIX dot-dot, то есть "/../". Эта уязвимость исправлена в 4.1.4."
Этот PoC демонстрирует, что metadata.Version используется для построения пути к архиву плагина, что позволяет осуществить обход пути и записать загруженный plugin.tgz за пределами предполагаемого каталога плагина. Содержимое плагина всё ещё извлекается в назначенный кэш-каталог перед установкой, поэтому данный PoC не демонстрирует произвольное извлечение или произвольную перезапись файлов хоста.
Уязвимость классифицируется как CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Установщик плагинов Helm имеет интерфейс с четырьмя реализациями (HTTP, local, 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/local установщик. Следовательно, для достижения обхода пути мы можем использовать http_installer, который получает плагины как архив, обслуживаемый веб-сервером.
В примере ниже бинарный файл helm был вручную пересобран для включения сообщений логирования, где вы можете наблюдать два разных потока данных. Первый — установка плагина с корректным именем версии. Второй — установка плагина, в имени версии которого есть разделители пути dot-dot:
./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()) остаются неизменными. Учитывая это, мы можем манипулировать местом назначения временного архива и его именем, что приводит к записи файла.
Вам не нужно ничего запускать или использовать post install webhooks, достаточно простой установки плагина, чтобы вызвать уязвимость.
Во время моего анализа HTTP-установщика я заметил, что извлечение архива происходит в i.CacheDir, тогда как установка (fs.CopyDir) копирует файлы из i.CacheDir в i.Path(). Я не нашёл пути в коде, где tarballPath, подверженный обходу, влияет на содержимое архива. Следовательно, основываясь на проанализированной трассировке во время выполнения, место назначения извлечения архива не зависит от tarballPath, контролируемого обходом.
В рекомендациях упоминаются как установка, так и обновление. Для установщиков HTTP/local/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