
PoC — autorizzazione mancante sull'archivio platform-wide delle trust-anchor GPG in Terrapod (GHSA-6qrc-597p-mrp9, CVE-2026-87006, CVSS 6.5).
Stato CVE: richiesto, in attesa di assegnazione. Questa segnalazione è pubblicata come GHSA-6qrc-597p-mrp9. All'assegnazione del CVE questo repository viene rinominato
CVE-YYYY-NNNNN-terrapod-PoCe questo banner viene sostituito con il link al CVE.
| Ricercatore | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-6qrc-597p-mrp9 |
| CVSS 3.1 | 6.5 (Medium) |
| Debolezza | CWE-862, CWE-284 |
Sommario
L'assenza di autorizzazione nell'API di gestione delle chiavi GPG in Terrapod (main @ b36d953, post-v1.3.1) consente a qualsiasi principal autenticato — incluso un utente con il solo ruolo integrato everyone e nessuna concessione di capability, o un token runner con scope limitato — di creare ed eliminare voci nell'archivio di trust-anchor GPG a livello di piattaforma utilizzato per verificare le firme su ogni provider pubblicato nel registry privato, tramite POST /api/terrapod/v1/gpg-keys e DELETE /api/terrapod/v1/gpg-keys/{key_id}.
Prodotto
Terrapod (mattrobinsonsre/terrapod) — sostituto self-hosted di Terraform Enterprise / HCP Terraform.
Versione testata
Commit b36d9535dedc31d85a02093d78f04da748492a2d (main), 10 commit avanti rispetto al tag di release più vicino v1.3.1. (v1.3.2 esiste come tag ma si trova sul branch release/v1.3 e non è ancora stato unito a main; il bug è presente su entrambi.)
CVSS v3.1 stimato
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N — 6.4 (Medium)
PR:L (non N): ogni richiesta necessita comunque di una credenziale valida — una sessione, un token API, o persino un token runner con scope di esecuzione runtok: — semplicemente non privilegiata. I:H/C:N/A:N: il bug consente a un chiamante non privilegiato di corrompere l'integrità dell'archivio condiviso delle chiavi di firma del registry (eliminare chiavi legittime, inserire le proprie), ma non divulga di per sé materiale di chiavi segrete (le chiavi private non vengono mai serializzate in alcuna risposta) né mette il servizio offline.
Dettagli
services/terrapod/api/routers/gpg_keys.py implementa le operazioni CRUD per le righe GPGKey — le chiavi pubbliche in formato ASCII-armored che Terrapod considera attendibili per verificare la firma detached SHA256SUMS.sig su ogni versione di provider pubblicata nel registry privato (services/terrapod/services/registry_provider_service.py:191-201, _verify_and_store_shasums_signature). Ogni route del router è protetta solo da Depends(get_current_user) — cioè "un qualche principal autenticato" — senza alcun controllo di ruolo o capability:
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-213Le funzioni di servizio sottostanti (services/terrapod/services/gpg_key_service.py:144 create_gpg_key, :316 delete_gpg_key) non accettano alcun argomento di chiamante/proprietà — GPGKey non ha colonne namespace o owner (_gpg_key_to_jsonapi codifica in modo fisso "namespace": "default"; il campo namespace accettato in creazione viene analizzato dal modello di richiesta ma non viene mai propagato). L'insieme di chiavi è un'unica lista globale e condivisa per l'intera piattaforma.
Confronta questo con ogni altra risorsa di piattaforma di proprietà admin nello stesso codebase — tokens.py, roles.py, vcs_connections.py, role_assignments.py — che proteggono tutte le route mutanti dietro require_admin o un controllo esplicito di proprietà bound_to == user.email or is_admin. gpg_keys.py è l'unico router in api/routers/ che gestisce una risorsa critica per la sicurezza a livello di piattaforma senza nulla di tutto ciò.
Catena di impatto: get_gpg_key_by_key_id() (registry_provider_service.py:195) esegue una ricerca globale e senza scope — qualsiasi chiave mai registrata, da chiunque, è un trust anchor valido per qualsiasi pubblicazione di provider (questo è by design per la registrazione self-service delle chiavi dei publisher — il messaggio di errore dice persino add it via /api/terrapod/v1/gpg-keys first). Poiché la registrazione non ha alcun controllo di autenticazione, e anche l'eliminazione non ne ha:
require_non_runner esiste specificamente per tenere lontano dagli "endpoint di creazione e gestione delle risorse" secondo la sua stessa docstring in services/terrapod/api/dependencies.py:387-400, ma che questo router non utilizza mai) può eliminare la chiave di firma registrata di qualsiasi altro tenant, rompendo la verifica della firma per ogni versione di provider che quel tenant ha già pubblicato — un attacco all'integrità cross-tenant che non richiede alcuna relazione con il namespace target.La suite di test del progetto stesso documenta questa lacuna senza metterla in discussione — services/tests/api/test_gpg_keys.py costruisce i suoi test happy-path di create/delete/revoke con AuthenticatedUser(roles=["everyone"], ...) (vedi _user(), riga 23, usato in tutto TestCreate/TestDelete/TestRevoke) e verifica 201/204 per quell'utente non privilegiato — cioè i test affermano il comportamento vulnerabile come corretto, semplicemente non sono mai stati scritti per chiedersi "dovrebbe everyone essere autorizzato a farlo?"
Proof of Concept
Verificato dinamicamente contro l'applicazione reale (Postgres + Redis reali tramite l'harness di integration-test del progetto stesso docker-compose.test.yml — nessun mock, nessuna modifica non correlata al codice dell'app):
services/tests/integration/test_gpg_key_missing_authz_poc.py:
test_everyone_role_user_can_delete_admins_signing_key — un admin registra una vera chiave pubblica PGP RSA-2048 tramite POST /api/terrapod/v1/gpg-keys (201, presenza confermata in Postgres), poi un secondo utente autenticato con soli roles=["everyone"] (nessun admin, nessuna concessione di capability) invia DELETE /api/terrapod/v1/gpg-keys/{key_id} e l'operazione riesce (204); la riga risulta poi assente da Postgres.test_everyone_role_user_can_register_new_trusted_key — lo stesso utente non privilegiato registra una chiave nuova di zecca tramite 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
Impatto
Qualsiasi utente autenticato di un'istanza Terrapod — indipendentemente da ruolo, accesso al workspace o permessi del registry, fino al ruolo integrato non privilegiato everyone o a un token runner con scope di una singola esecuzione — può manomettere l'archivio condiviso di trust-anchor GPG della piattaforma: eliminare la chiave di firma registrata di un altro team (rompendo la verifica della firma di terraform init per ogni versione di provider che ha già pubblicato, a livello di piattaforma) e/o aggiungere nuove chiavi all'insieme attendibile. Questo mina la garanzia di supply-chain "registry privato di moduli + provider firmati con GPG" che il progetto pubblicizza come funzionalità di punta, senza richiedere alcun privilegio di workspace o registry da parte dell'attaccante.
Debolezze
Remediation
Aggiungere un gate di capability/ruolo alle route mutanti in services/terrapod/api/routers/gpg_keys.py, seguendo il pattern già utilizzato da tokens.py/roles.py/vcs_connections.py — ad esempio Depends(require_admin) su create_gpg_key_endpoint, delete_gpg_key_endpoint e revoke_gpg_key_endpoint (revoke è separatamente protetto richiedendo un certificato di auto-revoca valido, ma non dovrebbe comunque essere raggiungibile da un chiamante arbitrario per le chiavi di altri tenant). list/show sono a rischio inferiore (i blocchi armor sono chiavi pubbliche by design) ma probabilmente dovrebbero anch'essi richiedere almeno require_non_runner per coerenza. Considerare anche l'introduzione di una colonna namespace/owner su GPGKey se il modello previsto è quello di chiavi publisher self-service per namespace, così che un owner di namespace possa gestire solo la propria chiave (o le proprie chiavi) invece dell'unica lista globale.
Crediti: Dostxodjayev Abdullox
Canale di segnalazione: Secondo SECURITY.md, non aprire una issue pubblica. Usa il private vulnerability reporting di GitHub: vai su https://github.com/mattrobinsonsre/terrapod/security/advisories/new, clicca "Report a vulnerability" e compila la descrizione, i passi per riprodurre e le versioni interessate. (Se il PVR non è disponibile, la policy indica di inviare un'email direttamente al maintainer.)
2 passedevidence/gpg_key_authz_poc_run3.png