
PoC — fehlende Autorisierung im plattformweiten GPG-Vertrauensanker-Speicher in Terrapod (GHSA-6qrc-597p-mrp9, CVE-2026-87006, CVSS 6.5).
CVE-Status: beantragt, Zuweisung ausstehend. Dieser Fund wird als GHSA-6qrc-597p-mrp9 veröffentlicht. Bei CVE-Zuweisung wird dieses Repository in
CVE-YYYY-NNNNN-terrapod-PoCumbenannt und dieses Banner durch den CVE-Link ersetzt.
| Researcher | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-6qrc-597p-mrp9 |
| CVSS 3.1 | 6.5 (Medium) |
| Weakness | CWE-862, CWE-284 |
Zusammenfassung
Fehlende Autorisierung in der GPG-Key-Management-API in Terrapod (main @ b36d953, nach v1.3.1) ermöglicht es jedem authentifizierten Principal — einschließlich eines Benutzers mit nur der integrierten everyone-Rolle und ohne jegliche Capability-Grants oder eines scoped Runner-Tokens —, Einträge im plattformweiten GPG-Trust-Anchor-Store zu erstellen und zu löschen, der zur Verifikation von Signaturen für jeden im privaten Registry veröffentlichten Provider verwendet wird, über POST /api/terrapod/v1/gpg-keys und DELETE /api/terrapod/v1/gpg-keys/{key_id}.
Produkt
Terrapod (mattrobinsonsre/terrapod) — selbstgehosteter Ersatz für Terraform Enterprise / HCP Terraform.
Getestete Version
Commit b36d9535dedc31d85a02093d78f04da748492a2d (main), 10 Commits vor dem nächstgelegenen Release-Tag v1.3.1. (v1.3.2 existiert als Tag, liegt aber auf dem Branch release/v1.3 und ist noch nicht in main gemerged; der Bug ist in beiden vorhanden.)
Geschätzter 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 (nicht N): jede Anfrage benötigt weiterhin eine gültige Credential — eine Session, ein API-Token oder sogar ein run-scoped runtok: Runner-Token — nur eben keine privilegierte. I:H/C:N/A:N: Der Bug erlaubt es einem unprivilegierten Aufrufer, die Integrität des gemeinsamen Signing-Key-Trust-Stores des Registry zu korrumpieren (legitime Keys löschen, eigene einfügen), legt aber selbst kein geheimes Schlüsselmaterial offen (private Schlüssel werden in keiner Antwort serialisiert) und nimmt den Dienst nicht offline.
Details
services/terrapod/api/routers/gpg_keys.py implementiert CRUD für GPGKey-Zeilen — die ASCII-armored öffentlichen Schlüssel, denen Terrapod vertraut, um die detached SHA256SUMS.sig für jede im privaten Registry veröffentlichte Provider-Version zu verifizieren (services/terrapod/services/registry_provider_service.py:191-201, _verify_and_store_shasums_signature). Jede Route im Router ist ausschließlich durch Depends(get_current_user) abgesichert — d. h. „irgendein authentifizierter Principal" — ohne jegliche Rollen- oder Capability-Prüfung:
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-213Die zugrunde liegenden Service-Funktionen (services/terrapod/services/gpg_key_service.py:144 create_gpg_key, :316 delete_gpg_key) nehmen überhaupt kein Caller-/Ownership-Argument entgegen — GPGKey hat keine Namespace- oder Owner-Spalte (_gpg_key_to_jsonapi hardcodet "namespace": "default"; das beim Erstellen akzeptierte namespace-Feld wird vom Request-Modell geparst, aber nie durchgereicht). Der Key-Satz ist eine einzige globale, gemeinsame Liste für die gesamte Plattform.
Vergleichen Sie dies mit jeder anderen admin-eigenen Plattform-Ressource in derselben Codebasis — tokens.py, roles.py, vcs_connections.py, role_assignments.py — die alle mutierende Routen hinter require_admin oder einer expliziten bound_to == user.email or is_admin-Ownership-Prüfung absichern. gpg_keys.py ist der einzige Router in api/routers/, der eine plattformweite sicherheitskritische Ressource ohne all das verwaltet.
Impact-Kette: get_gpg_key_by_key_id() (registry_provider_service.py:195) führt einen globalen, unscoped Lookup durch — jeder jemals von irgendwem registrierte Key ist ein gültiger Trust-Anchor für jede Provider-Veröffentlichung (dies ist by design für die Self-Service-Publisher-Key-Registrierung — die Fehlermeldung lautet sogar add it via /api/terrapod/v1/gpg-keys first). Da die Registrierung keine Auth-Prüfung hat und das Löschen ebenfalls keine:
require_non_runner speziell dafür existiert, um es gemäß seiner eigenen Docstring in services/terrapod/api/dependencies.py:387-400 von „resource creation and management endpoints" fernzuhalten, das dieser Router aber nie verwendet) kann den registrierten Signing-Key eines anderen Tenants löschen, wodurch die Signaturverifikation für jede von diesem Tenant bereits veröffentlichte Provider-Version bricht — ein Cross-Tenant-Integritätsangriff ohne jegliche Beziehung zum Ziel-Namespace.Die eigene Testsuite des Projekts dokumentiert diese Lücke, ohne sie zu hinterfragen — services/tests/api/test_gpg_keys.py baut seine Create/Delete/Revoke-Happy-Path-Tests mit AuthenticatedUser(roles=["everyone"], ...) auf (siehe _user(), Zeile 23, durchgängig in TestCreate/TestDelete/TestRevoke verwendet) und erwartet 201/204 für diesen unprivilegierten Benutzer — d. h. die Tests bestätigen das verwundbare Verhalten als korrekt, sie wurden nur nie geschrieben, um zu fragen „sollte everyone dazu berechtigt sein?"
Proof of Concept
Dynamisch gegen die reale Anwendung verifiziert (echtes Postgres + Redis über den eigenen Integrationstest-Harness des Projekts docker-compose.test.yml — keine Mocks, keine unbezogenen Änderungen am App-Code):
services/tests/integration/test_gpg_key_missing_authz_poc.py geschrieben:
test_everyone_role_user_can_delete_admins_signing_key — ein admin registriert einen echten RSA-2048-PGP-Public-Key via POST /api/terrapod/v1/gpg-keys (201, im Postgres als vorhanden bestätigt), dann sendet ein zweiter Benutzer, der nur mit roles=["everyone"] authentifiziert ist (kein Admin, keine Capability-Grants), DELETE /api/terrapod/v1/gpg-keys/{key_id} und es gelingt (204); die Zeile ist danach nachweislich aus Postgres verschwunden.test_everyone_role_user_can_register_new_trusted_key — derselbe unprivilegierte Benutzer registriert einen brandneuen Key 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
Auswirkung
Jeder authentifizierte Benutzer einer Terrapod-Instanz — unabhängig von Rolle, Workspace-Zugriff oder Registry-Berechtigungen, bis hinunter zur integrierten unprivilegierten everyone-Rolle oder einem einzelnen run-scoped Runner-Token — kann den gemeinsamen GPG-Trust-Anchor-Store der Plattform manipulieren: den registrierten Signing-Key eines anderen Teams löschen (wodurch die terraform init-Signaturverifikation für jede von ihm bereits veröffentlichte Provider-Version plattformweit bricht) und/oder neue Keys zum vertrauenswürdigen Satz hinzufügen. Dies untergräbt die „GPG-signed private module + provider registry"-Supply-Chain-Garantie, die das Projekt als Hauptfeature bewirbt, ohne dass der Angreifer irgendein Workspace- oder Registry-Privileg benötigt.
Schwachstellen
Behebung
Fügen Sie ein Capability-/Rollen-Gate zu den mutierenden Routen in services/terrapod/api/routers/gpg_keys.py hinzu, entsprechend dem bereits von tokens.py/roles.py/vcs_connections.py verwendeten Muster — z. B. Depends(require_admin) auf create_gpg_key_endpoint, delete_gpg_key_endpoint und revoke_gpg_key_endpoint (Revoke ist separat durch die Anforderung eines gültigen Self-Revocation-Zertifikats geschützt, sollte aber dennoch nicht für einen beliebigen Aufrufer für die Keys anderer Tenants erreichbar sein). list/show sind risikoärmer (die Armor-Blöcke sind by design öffentliche Schlüssel), sollten aber wahrscheinlich ebenfalls mindestens require_non_runner erfordern, um konsistent zu sein. Erwägen Sie zudem die Einführung einer Namespace-/Owner-Spalte auf GPGKey, falls Self-Service-Publisher-Keys pro Namespace das beabsichtigte Modell sind, damit ein Namespace-Owner nur seinen eigenen Key bzw. seine eigenen Keys verwalten kann statt der einen globalen Liste.
Credit: Dostxodjayev Abdullox
Meldeweg: Gemäß SECURITY.md kein öffentliches Issue eröffnen. Nutzen Sie GitHubs privates Vulnerability-Reporting: gehen Sie zu https://github.com/mattrobinsonsre/terrapod/security/advisories/new, klicken Sie auf „Report a vulnerability" und füllen Sie Beschreibung, Schritte zur Reproduktion und betroffene Versionen aus. (Falls PVR nicht verfügbar ist, sieht die Richtlinie vor, den Maintainer direkt per E-Mail zu kontaktieren.)
2 passedevidence/gpg_key_authz_poc_run3.png