
PoC — Terrapod의 플랫폼 전역 GPG 신뢰 앵커 저장소에서 누락된 인가 (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 |
요약
Terrapod(main @ b36d953, post-v1.3.1)의 GPG 키 관리 API에서 인가 누락이 발생하여, 내장된 everyone 역할만 가진 사용자나 범위가 제한된 러너 토큰을 포함한 모든 인증된 주체가 POST /api/terrapod/v1/gpg-keys 및 DELETE /api/terrapod/v1/gpg-keys/{key_id}를 통해 프라이빗 레지스트리에 게시된 모든 프로바이더의 서명을 검증하는 데 사용되는 플랫폼 전역 GPG 신뢰 앵커 저장소의 항목을 생성 및 삭제할 수 있습니다.
제품
Terrapod (mattrobinsonsre/terrapod) — 자체 호스팅 Terraform Enterprise / HCP Terraform 대체 제품.
테스트된 버전
커밋 b36d9535dedc31d85a02093d78f04da748492a2d (main), 가장 가까운 릴리스 태그 v1.3.1보다 10개 커밋 앞서 있음. (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 토큰, 또는 런 범위의 runtok: 러너 토큰 — 이 필요하지만, 권한이 있는 자격 증명일 필요는 없습니다. I:H/C:N/A:N: 이 버그는 권한 없는 호출자가 레지스트리의 공유 서명 키 신뢰 저장소의 무결성을 손상시킬 수 있지만(합법적인 키 삭제, 자체 키 삽입), 그 자체로 비밀 키 자료를 노출하거나(개인 키는 어떤 응답에도 직렬화되지 않음) 서비스를 오프라인으로 만들지는 않습니다.
세부 사항
services/terrapod/api/routers/gpg_keys.py는 GPGKey 행에 대한 CRUD를 구현합니다 — 이는 Terrapod가 프라이빗 레지스트리에 게시된 모든 프로바이더 버전의 분리된 SHA256SUMS.sig를 검증하기 위해 신뢰하는 ASCII-armored 공개 키입니다(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는 services/terrapod/api/dependencies.py:387-400의 자체 docstring에 따라 "리소스 생성 및 관리 엔드포인트"에서 이를 배제하기 위해 존재하지만, 이 라우터는 이를 사용하지 않음)가 다른 테넌트의 등록된 서명 키를 삭제하여 해당 테넌트가 이미 게시한 모든 프로바이더 버전의 서명 검증을 중단시킬 수 있습니다 — 대상 네임스페이스와의 관계가 전혀 필요 없는 교차 테넌트 무결성 공격입니다.프로젝트 자체의 테스트 스위트는 이 격차를 의문 없이 문서화합니다 — services/tests/api/test_gpg_keys.py는 AuthenticatedUser(roles=["everyone"], ...)로 생성/삭제/취소 해피 패스 테스트를 구성하며(_user(), 23행, TestCreate/TestDelete/TestRevoke 전반에 사용됨), 해당 권한 없는 사용자에 대해 201/204를 단언합니다 — 즉 테스트는 취약한 동작을 올바른 것으로 단언하며, "everyone이 이 작업을 수행하도록 허용해야 하는가?"라고 묻도록 작성된 적이 없습니다.
개념 증명
실제 애플리케이션에 대해 동적으로 검증됨(프로젝트 자체의 docker-compose.test.yml 통합 테스트 하네스를 통한 실제 Postgres + Redis — 모의 객체 없음, 앱 코드에 대한 무관한 수정 없음):
services/tests/integration/test_gpg_key_missing_authz_poc.py 작성:
test_everyone_role_user_can_delete_admins_signing_key — admin이 POST /api/terrapod/v1/gpg-keys를 통해 실제 RSA-2048 PGP 공개 키를 등록하고(201, Postgres에 존재 확인됨), 그런 다음 roles=["everyone"]만으로 인증된 두 번째 사용자(관리자 아님, 권한 부여 없음)가 DELETE /api/terrapod/v1/gpg-keys/{key_id}를 보내면 성공합니다(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
2 passed — 두 단언(권한 없는 삭제가 성공하고, 실제 데이터베이스에서 행이 실제로 사라짐)이 모두 유지되었습니다. 스크린샷: .영향
Terrapod 인스턴스의 모든 인증된 사용자 — 역할, 워크스페이스 접근 권한, 레지스트리 권한에 관계없이, 내장된 권한 없는 everyone 역할이나 단일 런의 범위가 제한된 러너 토큰에 이르기까지 — 가 플랫폼의 공유 GPG 신뢰 앵커 저장소를 변조할 수 있습니다: 다른 팀의 등록된 서명 키를 삭제하고(플랫폼 전역에서 이미 게시한 모든 프로바이더 버전의 terraform init 서명 검증을 중단시킴) 및/또는 신뢰 집합에 새 키를 추가할 수 있습니다. 이는 프로젝트가 대표 기능으로 광고하는 "GPG 서명된 프라이빗 모듈 + 프로바이더 레지스트리" 공급망 보장을 훼손하며, 공격자 측에서 워크스페이스나 레지스트리 권한이 전혀 필요하지 않습니다.
취약점
해결 방안
services/terrapod/api/routers/gpg_keys.py의 변경 라우트에 tokens.py/roles.py/vcs_connections.py에서 이미 사용되는 패턴과 일치하는 기능/역할 게이트를 추가하십시오 — 예를 들어 create_gpg_key_endpoint, delete_gpg_key_endpoint, revoke_gpg_key_endpoint에 Depends(require_admin)를 적용합니다(취소는 유효한 자체 취소 인증서를 요구함으로써 별도로 보호되지만, 다른 테넌트의 키에 대해 임의의 호출자가 접근할 수 없어야 합니다). list/show는 위험이 낮지만(armor 블록은 설계상 공개 키임) 일관성을 위해 최소한 require_non_runner를 요구해야 할 것입니다. 네임스페이스별 셀프 서비스 게시자 키가 의도된 모델이라면 GPGKey에 네임스페이스/소유자 컬럼을 도입하여 네임스페이스 소유자가 단일 전역 목록이 아닌 자신의 키만 관리할 수 있도록 하는 것도 고려하십시오.
크레딧: Dostxodjayev Abdullox
보고 채널: SECURITY.md에 따라 공개 이슈를 열지 마십시오. GitHub의 비공개 취약점 보고를 사용하십시오: https://github.com/mattrobinsonsre/terrapod/security/advisories/new로 이동하여 "Report a vulnerability"를 클릭하고 설명, 재현 단계, 영향을 받는 버전을 작성하십시오. (PVR을 사용할 수 없는 경우 정책에 따라 유지관리자에게 직접 이메일을 보내야 합니다.)
evidence/gpg_key_authz_poc_run3.png