
PoC — falta de autorización en el almacén de anclas de confianza GPG de toda la plataforma en Terrapod (GHSA-6qrc-597p-mrp9, CVE-2026-87006, CVSS 6.5).
Estado del CVE: solicitado, pendiente de asignación. Este hallazgo se publica como GHSA-6qrc-597p-mrp9. Tras la asignación del CVE, este repositorio se renombra
CVE-YYYY-NNNNN-terrapod-PoCy este banner se reemplaza con el enlace del CVE.
| Investigador | Dostxodjayev Abdullox (@squeeze440) |
| Aviso | GHSA-6qrc-597p-mrp9 |
| CVSS 3.1 | 6.5 (Medio) |
| Debilidad | CWE-862, CWE-284 |
Resumen
La falta de autorización en la API de gestión de claves GPG de Terrapod (main @ b36d953, posterior a v1.3.1) permite a cualquier principal autenticado —incluido un usuario con solo el rol integrado everyone y sin ninguna concesión de capacidades, o un token de runner con alcance limitado— crear y eliminar entradas en el almacén de anclas de confianza GPG de toda la plataforma, utilizado para verificar las firmas de todos los proveedores publicados en el registro privado, a través de POST /api/terrapod/v1/gpg-keys y DELETE /api/terrapod/v1/gpg-keys/{key_id}.
Producto
Terrapod (mattrobinsonsre/terrapod) — reemplazo autoalojado de Terraform Enterprise / HCP Terraform.
Versión probada
Commit b36d9535dedc31d85a02093d78f04da748492a2d (main), 10 commits por delante de la etiqueta de versión más cercana v1.3.1. (v1.3.2 existe como etiqueta pero se encuentra en la rama release/v1.3 y aún no se ha fusionado en main; el error está presente en ambas.)
CVSS v3.1 estimado
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N — 6.4 (Medio)
PR:L (no N): toda solicitud sigue necesitando una credencial válida —una sesión, un token de API o incluso un token de runner con alcance de ejecución runtok:—, solo que no una privilegiada. I:H/C:N/A:N: el error permite a un llamador sin privilegios corromper la integridad del almacén compartido de claves de firma del registro (eliminar claves legítimas, insertar las propias), pero no revela por sí mismo material de clave secreta (las claves privadas nunca se serializan en ninguna respuesta) ni deja el servicio fuera de línea.
Detalles
services/terrapod/api/routers/gpg_keys.py implementa el CRUD de las filas GPGKey —las claves públicas con armadura ASCII que Terrapod confía para verificar la firma separada SHA256SUMS.sig en cada versión de proveedor publicada en el registro privado (services/terrapod/services/registry_provider_service.py:191-201, _verify_and_store_shasums_signature). Todas las rutas del router están protegidas únicamente por Depends(get_current_user) —es decir, "algún principal autenticado"— sin ninguna comprobación de rol o capacidad:
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-213Las funciones de servicio subyacentes (services/terrapod/services/gpg_key_service.py:144 create_gpg_key, :316 delete_gpg_key) no reciben ningún argumento de llamador/propiedad —GPGKey no tiene columna de espacio de nombres ni de propietario (_gpg_key_to_jsonapi codifica de forma fija "namespace": "default"; el campo namespace aceptado en la creación es analizado por el modelo de solicitud pero nunca se transmite). El conjunto de claves es una única lista global y compartida para toda la plataforma.
Compárese con todos los demás recursos de plataforma propiedad del administrador en el mismo código base —tokens.py, roles.py, vcs_connections.py, role_assignments.py—, que protegen todas las rutas mutadoras detrás de require_admin o de una comprobación explícita de propiedad bound_to == user.email or is_admin. gpg_keys.py es el único router en api/routers/ que gestiona un recurso crítico para la seguridad de toda la plataforma sin nada de eso.
Cadena de impacto: get_gpg_key_by_key_id() (registry_provider_service.py:195) realiza una búsqueda global y sin alcance —cualquier clave registrada alguna vez, por quien sea, es un ancla de confianza válida para cualquier publicación de proveedor (esto es por diseño para el registro de claves de publicador de autoservicio— el mensaje de error incluso dice add it via /api/terrapod/v1/gpg-keys first). Dado que el registro no tiene comprobación de autenticación, y la eliminación tampoco:
require_non_runner existe específicamente para mantener fuera de los "endpoints de creación y gestión de recursos" según su propia docstring en services/terrapod/api/dependencies.py:387-400, pero que este router nunca utiliza) puede eliminar la clave de firma registrada de cualquier otro inquilino, rompiendo la verificación de firma de todas las versiones de proveedor que ese inquilino ya publicó —un ataque de integridad entre inquilinos que no requiere ninguna relación con el espacio de nombres objetivo.La propia suite de pruebas del proyecto documenta esta laguna sin cuestionarla —services/tests/api/test_gpg_keys.py construye sus pruebas de camino feliz de creación/eliminación/revocación con AuthenticatedUser(roles=["everyone"], ...) (véase _user(), línea 23, usado en todo TestCreate/TestDelete/TestRevoke) y afirma 201/204 para ese usuario sin privilegios—, es decir, las pruebas afirman el comportamiento vulnerable como correcto, simplemente nunca se escribieron para preguntar "¿debería permitirse a everyone hacer esto?"
Prueba de concepto
Verificado dinámicamente contra la aplicación real (Postgres + Redis reales mediante el propio arnés de pruebas de integración docker-compose.test.yml del proyecto —sin mocks, sin modificaciones ajenas al código de la aplicación):
services/tests/integration/test_gpg_key_missing_authz_poc.py:
test_everyone_role_user_can_delete_admins_signing_key — un admin registra una clave pública PGP RSA-2048 real mediante POST /api/terrapod/v1/gpg-keys (201, confirmada presente en Postgres), luego un segundo usuario autenticado solo con roles=["everyone"] (sin admin, sin concesiones de capacidades) envía DELETE /api/terrapod/v1/gpg-keys/{key_id} y tiene éxito (204); se confirma después que la fila ha desaparecido de Postgres.test_everyone_role_user_can_register_new_trusted_key — el mismo usuario sin privilegios registra una clave completamente nueva mediante 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
Impacto
Cualquier usuario autenticado de una instancia de Terrapod —independientemente de su rol, acceso al workspace o permisos de registro, hasta el rol integrado sin privilegios everyone o el token de runner con alcance de una sola ejecución— puede manipular el almacén compartido de anclas de confianza GPG de la plataforma: eliminar la clave de firma registrada de otro equipo (rompiendo la verificación de firma de terraform init para todas las versiones de proveedor que ya publicó, en toda la plataforma) y/o añadir nuevas claves al conjunto de confianza. Esto socava la garantía de cadena de suministro de "registro privado de módulos + proveedores firmados con GPG" que el proyecto anuncia como característica destacada, sin requerir ningún privilegio de workspace o registro por parte del atacante.
Debilidades
Remediación
Añadir una puerta de capacidad/rol a las rutas mutadoras en services/terrapod/api/routers/gpg_keys.py, siguiendo el patrón ya utilizado por tokens.py/roles.py/vcs_connections.py —por ejemplo, Depends(require_admin) en create_gpg_key_endpoint, delete_gpg_key_endpoint y revoke_gpg_key_endpoint (revoke está protegido por separado al requerir un certificado de autorrevocación válido, pero aun así no debería ser accesible por un llamador arbitrario para las claves de otros inquilinos). list/show tienen menor riesgo (los bloques con armadura son claves públicas por diseño), pero probablemente también deberían requerir al menos require_non_runner por coherencia. Considérese también introducir una columna de espacio de nombres/propietario en GPGKey si el modelo previsto son claves de publicador de autoservicio por espacio de nombres, de modo que el propietario de un espacio de nombres pueda gestionar solo su(s) propia(s) clave(s) en lugar de la única lista global.
Crédito: Dostxodjayev Abdullox
Canal de notificación: Según SECURITY.md, no abrir un issue público. Utilizar el informe privado de vulnerabilidades de GitHub: ir a https://github.com/mattrobinsonsre/terrapod/security/advisories/new, hacer clic en "Report a vulnerability" y rellenar la descripción, los pasos para reproducir y las versiones afectadas. (Si el PVR no está disponible, la política indica enviar un correo electrónico directamente al mantenedor).
2 passedevidence/gpg_key_authz_poc_run3.png