CASE · 軽微 · monitoring-gap
未検証の報告公開情報からの収集自作のセキュリティガードが3か所で素通りしていた(未検証の報告)
筆者によれば、AIエージェントに実装からマージまで任せる環境で、.env を読ませないフックが node -e で、main への force push を止めるフックが改行を挟んだ2行目で、それぞれ素通りしていた。レビュー役のAIがPRの設定ファイルに操られうる構造も見つかった。筆者の自己点検で見つかった話で、実際の被害には触れていない。
約6分で読めます
- 発生日
- 分類
- monitoring-gap
- 影響範囲
- none
- 検知
- self-audit
- タグ
- #claude-code #codex #hooks #env-file #force-push #shlex #poisoned-pipeline-execution #ai-code-review #guardrails
CH 1
原因
筆者によれば、.env のフックは読み取りに使う道具の名前を並べて判定していたため、並べていない道具では止まらなかった。force push のフックは、Pythonのshlexが改行を空白として扱うことを見落としていた。レビュー役のCLIは起動したディレクトリの設定を読むが、PRのheadには提出者が好きなファイルを置ける。
CH 2
結果
筆者はnode -eで .env.local の内容が実際に読めることを確かめた。改行で区切った git push -f origin main もテストで止まらなかった。ブランチ名を得るコマンドが呼び出し元のcwdに依存し、リポジトリ外から呼ぶと判定が黙って抜ける問題もあった。レビュー役の問題は、実際に破られたわけではないと筆者は述べている。記事に実害の記載はない。
CH 3
対策
筆者は、道具名ではなく .env 系のファイル名で止める方式を既存ルールに足し、改行をコマンドの区切りとして扱うよう直し、ブランチ判定をcwdに依存させなかった。誤検知を避けるため、中身を見るのは実際に実行される文字列に絞り、止まるものと止まってはいけないものを同じテスト表に並べてCIで回した。レビュー役には、PRの設定ファイルを消した作業コピーなど6つの対策を重ねた。
何が起きたか
筆者は、Claude Code・Codex・Cursor Agentを使い、ブランチ、PR、CIを経て AIがマージするところまで任せている。.env の実値の読み取りと、mainへの force push・mainの削除は、機械で止める設定にしていた。筆者によれば、 その止める側に3つの穴があった。いずれも筆者が自分のガードを点検して 見つけたもので、記事は実害を挙げていない。
現場の混乱
9月20日の朝、筆者が本番投入の準備で .env.local の値をSecret Managerに登録する 作業をAIにやらせようとしたところ、フックに止められた。これは設計どおりだった。 念のため別の道具で試すと、node -e でファイルを読む形が通り、 内容が読めた。
2つ目は、このフックのテストをCIに入れようとして見つかった。 1行で書いた git status; git push -f origin main は止まるが、 改行で2行に分けた同じ内容は止まらなかった。筆者によれば、AIは複数の コマンドを改行でつないで1回のBash実行に渡すことが多い。 さらに、ブランチ名を見るコマンドが呼び出し元のcwdに依存しており、 cwdがリポジトリ外だとブランチ名が空になって判定が抜けた。エラーは出ず、 黙って通すだけだった。
3つ目は10月7日の朝、Cloudflareが公開しているsecurity-auditスキルで、 共通ルールのリポジトリを監査したときに出た。コードは動かさず ソースを読むだけのquickプロファイルで、確定した脆弱性は0件だったが、 「要検証」の1番目にレビュー役の件が挙がった。筆者は同じ朝のうちに塞いだ。
原因
筆者の説明では、.env のフックは cat、grep、sed、xxd など読み取りに使う 道具の名前を並べる方式だった。node -e、python3 -c、base64、cp など ファイルを読む方法は他にもあり、AIはある道具が止められると、 タスクを終えるために別の道を探すだけで、悪意があるわけではないという。 permissions.deny の Read(.env) はClaude Codeのファイル読み取りを 止めるもので、Bashの中で動く任意のプログラムがファイルを開く経路までは 見ていない。
force pushのフックはshlexでコマンドを字句に分けていたが、shlexの既定では 改行が空白として扱われる。; や && は区切りとして見ていたのに、 改行だけが空白になり、2行が1つのコマンドとして連なって、先頭の git status だけが判定されていた。
レビュー役については、Claude CodeもCodexも起動したディレクトリの設定 (.claude/settings.json のフック、.mcp.json のMCPサーバー、.codex/ のルール)を 読む。PRのheadには提出者が好きなファイルを置けるため、.mcp.json を1枚足せば、 レビュー役の起動時に任意のコマンドが動きうる。筆者は、AIが書くPRはAIが読んだ Issueやwebページ、依存パッケージのREADMEの影響を受けるので、 プロンプトインジェクションで設定ファイルを混ぜられうると述べ、 OWASPのCICD-SEC-4(Poisoned Pipeline Execution)と同じ形だとしている。
対策
.env のフックは、道具の名前ではなく、コマンドに現れる .env 系のファイル名で 止める判定を足した(.env.example だけは通す)。既存のルールは消していない。 合成した15件のコマンドで、止まる8件と通る7件が期待どおりになることを確かめた。 ただし、ディスク上のスクリプトが中で .env を読む場合はコマンド文字列に 現れず止められない。筆者はこの限界をCLAUDE.mdに明記し、読ませたくない値は 開発機に置かず、本番の値はSecret Managerに置くとしている。
force pushのフックは、改行を空白ではなくコマンドの区切りとして扱うよう 直した。直したところで、コミットメッセージの文字列まで誤検知したため、 中を見る対象を、bash -c、eval、ssh、シェルに渡すヒアドキュメントなど、 実際に実行される文字列に絞った。止まるものと止まってはいけないものを 同じ表にしたフックのテストをCIに入れ、止まるもの23件・止まらないもの17件の 40件が通過している。ブランチ判定は、スクリプトの場所からプロジェクトの ルートを求める形にし、cwdに依存させなかった。
レビュー役には6つの対策を重ねた。PRのheadをホーム配下のProjectsの外に チェックアウトする。作業コピーから .claude/、.codex/、.cursor/、.mcp.json を 消してから呼ぶ。プロジェクト設定とMCPを読まないオプションを付けて起動する。 審査ルールはベース側の版を使う。レビュー結果は伏せ字フィルタを通す。 qa-pass を付ける直前にPRのheadが変わっていないか見比べる。 筆者は、通知には「リポジトリ名、PR番号とタイトル、完了・失敗・待ち」の1行だけを 送るようにした。
教訓として、ガードは書いただけでは効いているか分からず、自分で破ってみて 確かめる必要がある。フックの不具合は、止めすぎて騒ぐ形ではなく、 黙って通す形で現れる。
出典
この記録をチームに回す