
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 ロールのみを持ちケイパビリティ付与が一切ないユーザーや、スコープ付きランナートークンを含む — が、プライベートレジストリに公開されるすべてのプロバイダーの署名検証に使用されるプラットフォーム全体のGPGトラストアンカーストアのエントリを、POST /api/terrapod/v1/gpg-keys および DELETE /api/terrapod/v1/gpg-keys/{key_id} を介して作成・削除できるようになります。
製品
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を実装しています — これは、プライベートレジストリに公開されるすべてのプロバイダーバージョンのデタッチド SHA256SUMS.sig を検証するためにTerrapodが信頼するASCIIアーマー形式の公開鍵です (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"] のみで認証された2番目のユーザー (管理者なし、ケイパビリティ付与なし) が 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 — 両方のアサーション (不正な削除が成功すること、および行が実際のデータベースから実際に消えること) が成立しました。スクリーンショット: evidence/gpg_key_authz_poc_run3.png。影響
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) を付けます (revokeは有効な自己失効証明書を要求することで別途保護されていますが、それでも他のテナントの鍵に対して任意の呼び出し元から到達可能であるべきではありません)。list/show はリスクが低い (アーマーブロックは設計上公開鍵です) ですが、一貫性のために少なくとも require_non_runner を要求すべきでしょう。名前空間ごとのセルフサービスパブリッシャー鍵が意図されたモデルである場合は、GPGKey に名前空間/所有者カラムを導入することも検討してください。そうすれば、名前空間の所有者は単一のグローバルリストではなく自身の鍵のみを管理できます。
クレジット: Dostxodjayev Abdullox
報告チャネル: SECURITY.md に従い、公開Issueを開かないでください。GitHubのプライベート脆弱性報告を使用してください: https://github.com/mattrobinsonsre/terrapod/security/advisories/new にアクセスし、「Report a vulnerability」をクリックして、説明、再現手順、影響を受けるバージョンを記入してください。(PVRが利用できない場合、ポリシーではメンテナーに直接メールするよう定められています。)