
PoC — отсутствие авторизации в общеплатформенном хранилище доверенных якорей GPG в Terrapod (GHSA-6qrc-597p-mrp9, CVE-2026-87006, CVSS 6.5).
Статус CVE: запрошен, ожидает присвоения. Эта находка опубликована как GHSA-6qrc-597p-mrp9. После присвоения CVE этот репозиторий будет переименован в
CVE-YYYY-NNNNN-terrapod-PoC, а этот баннер заменён ссылкой на CVE.
| Исследователь | Dostxodjayev Abdullox (@squeeze440) |
| Уведомление | GHSA-6qrc-597p-mrp9 |
| CVSS 3.1 | 6.5 (Medium) |
| Слабость | CWE-862, CWE-284 |
Краткое описание
Отсутствие авторизации в API управления GPG-ключами в Terrapod (main @ b36d953, после v1.3.1) позволяет любому аутентифицированному субъекту — включая пользователя с единственной встроенной ролью everyone и без каких-либо предоставленных прав, либо ограниченный токен runner — создавать и удалять записи в общеплатформенном хранилище доверенных GPG-якорей, используемом для проверки подписей на каждом провайдере, публикуемом в приватный реестр, через POST /api/terrapod/v1/gpg-keys и DELETE /api/terrapod/v1/gpg-keys/{key_id}.
Продукт
Terrapod (mattrobinsonsre/terrapod) — самостоятельно размещаемая замена Terraform Enterprise / HCP Terraform.
Протестированная версия
Коммит b36d9535dedc31d85a02093d78f04da748492a2d (main), на 10 коммитов впереди ближайшего релизного тега v1.3.1. (v1.3.2 существует как тег, но находится в ветке release/v1.3 и ещё не слит в main; ошибка присутствует в обеих.)
Оценка CVSS v3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N — 6.4 (Medium)
PR:L (не N): каждый запрос всё ещё требует действительных учётных данных — сессии, API-токена или даже ограниченного запуском токена runner runtok: — просто не привилегированных. I:H/C:N/A:N: ошибка позволяет непривилегированному вызывающему нарушить целостность общего хранилища доверенных ключей подписи реестра (удалить легитимные ключи, вставить свои), но сама по себе не раскрывает секретный ключевой материал (приватные ключи никогда не сериализуются ни в одном ответе) и не выводит сервис из строя.
Детали
services/terrapod/api/routers/gpg_keys.py реализует CRUD для строк GPGKey — публичных ключей в ASCII-armor, которым Terrapod доверяет при проверке отсоединённой подписи SHA256SUMS.sig на каждой версии провайдера, публикуемой в приватный реестр (services/terrapod/services/registry_provider_service.py:191-201, _verify_and_store_shasums_signature). Каждый маршрут в роутере защищён только Depends(get_current_user) — то есть «некоторым аутентифицированным субъектом» — без какой-либо проверки роли или прав:
create_gpg_key_endpoint — services/terrapod/api/routers/gpg_keys.py:104-130list_gpg_keys_endpoint — gpg_keys.py:133-146show_gpg_key_endpoint — gpg_keys.py:149-166revoke_gpg_key_endpoint — gpg_keys.py:168-193delete_gpg_key_endpoint — gpg_keys.py:195-213Базовые сервисные функции (services/terrapod/services/gpg_key_service.py:144 create_gpg_key, :316 delete_gpg_key) вообще не принимают аргумент вызывающего/владельца — у GPGKey нет столбца пространства имён или владельца (_gpg_key_to_jsonapi жёстко задаёт "namespace": "default"; поле namespace, принимаемое при создании, разбирается моделью запроса, но никогда не передаётся дальше). Набор ключей — это один глобальный общий список для всей платформы.
Сравните это с любым другим административным ресурсом платформы в той же кодовой базе — tokens.py, roles.py, vcs_connections.py, role_assignments.py — которые все защищают изменяющие маршруты за require_admin или явной проверкой владения bound_to == user.email or is_admin. gpg_keys.py — единственный роутер в api/routers/, который управляет общеплатформенным критичным для безопасности ресурсом без всего этого.
Цепочка воздействия: get_gpg_key_by_key_id() (registry_provider_service.py:195) выполняет глобальный поиск без привязки к пространству имён — любой ключ, когда-либо зарегистрированный кем угодно, является действительным доверенным якорем для публикации любого провайдера (это сделано намеренно для самостоятельной регистрации ключей издателя — сообщение об ошибке даже гласит add it via /api/terrapod/v1/gpg-keys first). Поскольку регистрация не имеет проверки авторизации, а удаление тоже не имеет проверки авторизации:
require_non_runner существует специально, чтобы не допускать к «endpoints создания и управления ресурсами» согласно его собственному docstring в services/terrapod/api/dependencies.py:387-400, но который этот роутер никогда не использует) может удалить зарегистрированный ключ подписи любого другого арендатора, нарушив проверку подписи для каждой версии провайдера, которую этот арендатор уже опубликовал — межарендаторная атака на целостность, не требующая никакой связи с целевым пространством имён.Собственный набор тестов проекта документирует этот пробел, не подвергая его сомнению — services/tests/api/test_gpg_keys.py строит свои happy-path тесты create/delete/revoke с AuthenticatedUser(roles=["everyone"], ...) (см. _user(), строка 23, используется повсюду в TestCreate/TestDelete/TestRevoke) и утверждает 201/204 для этого непривилегированного пользователя — то есть тесты утверждают уязвимое поведение как корректное, они просто никогда не были написаны так, чтобы задать вопрос «должна ли роль everyone иметь право это делать?»
Доказательство концепции
Динамически проверено на реальном приложении (реальные Postgres + Redis через собственный интеграционный тестовый стенд проекта docker-compose.test.yml — без моков, без посторонних модификаций кода приложения):
services/tests/integration/test_gpg_key_missing_authz_poc.py:
test_everyone_role_user_can_delete_admins_signing_key — admin регистрирует реальный публичный PGP-ключ RSA-2048 через POST /api/terrapod/v1/gpg-keys (201, подтверждено наличие в Postgres), затем второй пользователь, аутентифицированный только с roles=["everyone"] (без admin, без предоставленных прав), отправляет DELETE /api/terrapod/v1/gpg-keys/{key_id}, и это succeeds (204); строка подтверждённо исчезает из Postgres после этого.test_everyone_role_user_can_register_new_trusted_key — тот же непривилегированный пользователь регистрирует совершенно новый ключ через POST /api/terrapod/v1/gpg-keys (201).docker build -f docker/Dockerfile.test -t terrapod-test:local .
docker compose -f docker-compose.test.yml run --rm test \
pytest tests/integration/test_gpg_key_missing_authz_poc.py -v -m integration
Воздействие
Любой аутентифицированный пользователь экземпляра Terrapod — независимо от роли, доступа к рабочему пространству или прав в реестре, вплоть до встроенной непривилегированной роли everyone или ограниченного одним запуском токена runner — может подменить общее хранилище доверенных GPG-якорей платформы: удалить зарегистрированный ключ подписи другой команды (нарушив проверку подписи terraform init для каждой версии провайдера, которую они уже опубликовали, в масштабе всей платформы) и/или добавить новые ключи в доверенный набор. Это подрывает гарантию цепочки поставок «приватный реестр модулей + провайдеров с GPG-подписью», которую проект рекламирует как ключевую функцию, не требуя от атакующего каких-либо привилегий в рабочем пространстве или реестре.
Слабости
Устранение
Добавьте проверку прав/роли на изменяющие маршруты в services/terrapod/api/routers/gpg_keys.py, следуя шаблону, уже используемому в tokens.py/roles.py/vcs_connections.py — например, Depends(require_admin) на create_gpg_key_endpoint, delete_gpg_key_endpoint и revoke_gpg_key_endpoint (revoke отдельно защищён требованием действительного сертификата самоподписи, но всё равно не должен быть доступен произвольному вызывающему для ключей других арендаторов). list/show менее рискованны (блоки armor по своей природе являются публичными ключами), но, вероятно, также должны требовать как минимум require_non_runner для согласованности. Рассмотрите также введение столбца пространства имён/владельца в GPGKey, если предполагаемая модель — самостоятельное управление ключами издателя по пространствам имён, чтобы владелец пространства имён мог управлять только своим ключом(ами), а не единым глобальным списком.
Благодарность: Dostxodjayev Abdullox
Канал сообщения об уязвимости: Согласно SECURITY.md, не открывайте публичный issue. Используйте приватное сообщение об уязвимостях GitHub: перейдите на https://github.com/mattrobinsonsre/terrapod/security/advisories/new, нажмите «Report a vulnerability» и заполните описание, шаги воспроизведения и затронутые версии. (Если PVR недоступен, политика предписывает написать мейнтейнеру напрямую.)
2 passedevidence/gpg_key_authz_poc_run3.png