
PoC — autorisation manquante sur le magasin d'ancres de confiance GPG à l'échelle de la plateforme dans Terrapod (GHSA-6qrc-597p-mrp9, CVE-2026-87006, CVSS 6.5).
Statut CVE : demandé, en attente d'attribution. Cette découverte est publiée sous GHSA-6qrc-597p-mrp9. À l'attribution de la CVE, ce dépôt est renommé
CVE-YYYY-NNNNN-terrapod-PoCet cette bannière est remplacée par le lien CVE.
| Chercheur | Dostxodjayev Abdullox (@squeeze440) |
| Avis | GHSA-6qrc-597p-mrp9 |
| CVSS 3.1 | 6.5 (Moyen) |
| Faiblesse | CWE-862, CWE-284 |
Résumé
L'absence d'autorisation dans l'API de gestion des clés GPG de Terrapod (main @ b36d953, post-v1.3.1) permet à tout principal authentifié — y compris un utilisateur disposant uniquement du rôle intégré everyone et d'aucune attribution de capacité, ou un jeton de runner à portée limitée — de créer et supprimer des entrées dans le magasin d'ancres de confiance GPG à l'échelle de la plateforme, utilisé pour vérifier les signatures de chaque fournisseur publié dans le registre privé, via POST /api/terrapod/v1/gpg-keys et DELETE /api/terrapod/v1/gpg-keys/{key_id}.
Produit
Terrapod (mattrobinsonsre/terrapod) — remplacement auto-hébergé de Terraform Enterprise / HCP Terraform.
Version testée
Commit b36d9535dedc31d85a02093d78f04da748492a2d (main), 10 commits en avance sur le tag de version le plus proche v1.3.1. (v1.3.2 existe en tant que tag mais se situe sur la branche release/v1.3 et n'est pas encore fusionné dans main ; le bug est présent sur les deux.)
CVSS v3.1 estimé
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N — 6.4 (Moyen)
PR:L (et non N) : chaque requête nécessite toujours un identifiant valide — une session, un jeton d'API, ou même un jeton de runner à portée d'exécution runtok: — mais pas un identifiant privilégié. I:H/C:N/A:N : le bug permet à un appelant non privilégié de corrompre l'intégrité du magasin d'ancres de confiance partagé du registre (supprimer des clés légitimes, insérer les siennes), mais ne divulgue pas en soi de matériel de clé secrète (les clés privées ne sont jamais sérialisées dans aucune réponse) et ne met pas le service hors ligne.
Détails
services/terrapod/api/routers/gpg_keys.py implémente le CRUD pour les lignes GPGKey — les clés publiques au format ASCII-armored que Terrapod considère comme fiables pour vérifier la signature détachée SHA256SUMS.sig sur chaque version de fournisseur publiée dans le registre privé (services/terrapod/services/registry_provider_service.py:191-201, _verify_and_store_shasums_signature). Chaque route du routeur n'est protégée que par Depends(get_current_user) — c'est-à-dire « un principal authentifié quelconque » — sans aucune vérification de rôle ou de capacité :
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-213Les fonctions de service sous-jacentes (services/terrapod/services/gpg_key_service.py:144 create_gpg_key, :316 delete_gpg_key) ne prennent aucun argument d'appelant ou de propriété — GPGKey n'a ni colonne de namespace ni de propriétaire (_gpg_key_to_jsonapi code en dur "namespace": "default" ; le champ namespace accepté à la création est analysé par le modèle de requête mais jamais transmis). L'ensemble des clés est une liste globale et partagée pour toute la plateforme.
Comparez cela avec toutes les autres ressources de plateforme détenues par l'administrateur dans le même codebase — tokens.py, roles.py, vcs_connections.py, role_assignments.py — qui protègent toutes les routes de mutation derrière require_admin ou une vérification explicite de propriété bound_to == user.email or is_admin. gpg_keys.py est le seul routeur dans api/routers/ qui gère une ressource critique pour la sécurité à l'échelle de la plateforme sans rien de tout cela.
Chaîne d'impact : get_gpg_key_by_key_id() (registry_provider_service.py:195) effectue une recherche globale et non délimitée — toute clé jamais enregistrée, par n'importe qui, est une ancre de confiance valide pour n'importe quelle publication de fournisseur (c'est intentionnel pour l'enregistrement en libre-service des clés de publication — le message d'erreur indique même add it via /api/terrapod/v1/gpg-keys first). Comme l'enregistrement n'a aucune vérification d'authentification, et que la suppression n'en a aucune non plus :
require_non_runner existe spécifiquement pour tenir à l'écart des « resource creation and management endpoints » selon sa propre docstring dans services/terrapod/api/dependencies.py:387-400, mais que ce routeur n'utilise jamais) peut supprimer la clé de signature enregistrée de tout autre locataire, brisant la vérification de signature pour chaque version de fournisseur que ce locataire a déjà publiée — une attaque d'intégrité inter-locataires sans aucune relation requise avec le namespace cible.La propre suite de tests du projet documente cette lacune sans la remettre en question — services/tests/api/test_gpg_keys.py construit ses tests de chemin heureux create/delete/revoke avec AuthenticatedUser(roles=["everyone"], ...) (voir _user(), ligne 23, utilisé partout dans TestCreate/TestDelete/TestRevoke) et affirme 201/204 pour cet utilisateur non privilégié — c'est-à-dire que les tests affirment le comportement vulnérable comme correct, ils n'ont simplement jamais été écrits pour demander « everyone devrait-il être autorisé à faire cela ? »
Preuve de concept
Vérifié dynamiquement contre l'application réelle (vrai Postgres + Redis via le propre harnais de tests d'intégration docker-compose.test.yml du projet — aucun mock, aucune modification non liée au code de l'application) :
services/tests/integration/test_gpg_key_missing_authz_poc.py :
test_everyone_role_user_can_delete_admins_signing_key — un admin enregistre une vraie clé publique PGP RSA-2048 via POST /api/terrapod/v1/gpg-keys (201, présence confirmée dans Postgres), puis un second utilisateur authentifié avec roles=["everyone"] uniquement (pas d'admin, aucune attribution de capacité) envoie DELETE /api/terrapod/v1/gpg-keys/{key_id} et cela réussit (204) ; la ligne est confirmée absente de Postgres par la suite.test_everyone_role_user_can_register_new_trusted_key — le même utilisateur non privilégié enregistre une toute nouvelle clé via 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
Impact
Tout utilisateur authentifié d'une instance Terrapod — quel que soit son rôle, son accès aux workspaces ou ses permissions de registre, jusqu'au rôle intégré non privilégié everyone ou au jeton de runner à portée d'une seule exécution — peut altérer le magasin partagé d'ancres de confiance GPG de la plateforme : supprimer la clé de signature enregistrée d'une autre équipe (brisant la vérification de signature terraform init pour chaque version de fournisseur qu'elle a déjà publiée, à l'échelle de la plateforme) et/ou ajouter de nouvelles clés à l'ensemble de confiance. Cela compromet la garantie de chaîne d'approvisionnement « registre privé de modules + fournisseurs signés GPG » que le projet présente comme une fonctionnalité phare, sans exiger de privilège de workspace ou de registre de la part de l'attaquant.
Faiblesses
Remédiation
Ajouter une barrière de capacité/rôle aux routes de mutation dans services/terrapod/api/routers/gpg_keys.py, en suivant le modèle déjà utilisé par tokens.py/roles.py/vcs_connections.py — par exemple Depends(require_admin) sur create_gpg_key_endpoint, delete_gpg_key_endpoint et revoke_gpg_key_endpoint (revoke est protégé séparément en exigeant un certificat d'auto-révocation valide, mais ne devrait toujours pas être accessible à un appelant arbitraire pour les clés d'autres locataires). list/show présentent un risque moindre (les blocs armor sont des clés publiques par conception) mais devraient probablement aussi exiger au moins require_non_runner par cohérence. Envisager également d'introduire une colonne de namespace/propriétaire sur GPGKey si des clés de publication en libre-service par namespace constituent le modèle prévu, afin qu'un propriétaire de namespace puisse gérer uniquement sa ou ses propres clés plutôt que la liste globale unique.
Crédit : Dostxodjayev Abdullox
Canal de signalement : Conformément à SECURITY.md, ne pas ouvrir d'issue publique. Utiliser le signalement privé de vulnérabilité de GitHub : rendez-vous sur https://github.com/mattrobinsonsre/terrapod/security/advisories/new, cliquez sur « Report a vulnerability », et remplissez la description, les étapes de reproduction et les versions affectées. (Si le PVR n'est pas disponible, la politique indique d'envoyer un courriel directement au mainteneur.)
2 passedevidence/gpg_key_authz_poc_run3.png