危険な操作を未然に止めるために、コマンドを 文字列一致(substring / prefix マッチ) で拒否する deny ルールを書く——よくある防御です。AI エージェントに破壊的な操作をさせないためのガードレールとしても、まず手が伸びる仕組みです。
ところが、ある PR をレビューしていて、この deny ルールに構造的な穴があることに気づきました。ルールは確かに存在し、テストもあり、一見すると「守れている」。しかし実際には、フラグの位置を変えるだけ、あるいは別の API バリアントを使うだけで、いとも簡単にすり抜けられたのです。
本記事は、「文字列一致の deny は多層防御の1層にすぎない」「テストが存在確認しかしていないと誤った安心感を生む」という学びの記録です。
事の発端:denyルールはあるのに、別経路で素通りする
レビュー対象には、危険な操作(保護されたブランチへの破壊的なマージ操作を想定してください)を止めるための deny ルールが設定されていました。設定は「禁止したいコマンド文字列」を列挙し、それに前方一致したら拒否する、という素直な作りです。
1# 設定イメージ(前方一致で拒否)2deny:3 - "version_control_mutation_cmd merge_target:*" # 保護対象へのマージを拒否4 - "api_mutation_cmd *resource/*/merge*" # REST経由のマージAPIを拒否5allow:6 - "version_control_mutation_cmd:*" # ただしツールは広く許可7 - "api_client:*"PR 本文は「これで危険なマージ経路は封鎖済み」と認識していました。実機検証もしていて、確かに拒否が発火しているように見えました。ところが、deny の網羅性を独立した観点で精査したところ、封鎖済みと認識されている経路の中に穴が見つかったのです。
穴その1:フラグを前置すると前方一致がすり抜ける
最初の穴は、フラグの位置でした。
deny ルール version_control_mutation_cmd merge_target:* は、コマンド文字列の 前方一致です。つまり「merge_target の直後にスペース」という語順を前提にしています。ところが、同じ操作はフラグを先に置いても成立します。
1version_control_mutation_cmd merge_target # ← deny に一致して拒否される2version_control_mutation_cmd --no-fast-forward merge_target # ← フラグ前置で deny 不一致、素通り3version_control_mutation_cmd -m "message" merge_target # ← 同上最初の形は deny にヒットしますが、フラグをターゲット名の前に置いた最も自然な語順は、前方一致から外れて allow 側(広く許可されたツール)で素通りしてしまいます。実機検証でたまたまフラグを後置していたために deny が発火して見え、「守れている」と誤認していた、というのが実態でした。
文字列の前方一致は、引数の語順という、コマンド仕様上いくらでも変えられる要素に依存します。語順を変えるだけでバイパスできるなら、それは防御として穴だらけです。
穴その2:別のAPIバリアントはパスにキーワードを含まない
ふたつめの穴は、API のバリアントでした。
deny ルール api_mutation_cmd *resource/*/merge* は、REST 風のパス(.../resource/123/merge のような形)にマッチさせる設計でした。ところが、同じ「マージする」という操作は、別の API バリアント——たとえば GraphQL の mutation——経由でも実行できます。
1# REST 経由:パスに "merge" を含む → deny にヒット2api_mutation_cmd PUT resource/123/merge3
4# GraphQL 経由:パスは汎用エンドポイント、操作名はクエリ本文の中5api_variant_mutation -f query='mutation { mergeTarget(...) { ... } }'GraphQL 経由のマージは、URL パスに resource/.../merge という文字列を含みません。操作の意図はクエリ本文の中にあるからです。結果として、REST パスを狙った deny ルールはこの経路を一切捕捉できず、allow の広いツール許可で素通りします。
「同じ操作に複数の到達経路がある」というのは、本番ガードの設計で何度も出てくる落とし穴です。文字列一致の deny は、列挙した1つの経路の1つの表記しか塞げません。別バリアントは設計者の視界の外で素通りします。
誤った安心感:テストが「存在」しか確認していない
このケースで最も危険だったのは、穴そのものよりも、テストが「守れている」という誤った安心感を生んでいたことでした。
deny ルールには対応するテストがありました。しかしそのテストは、version_control_mutation_cmd merge_target:* という deny ルールが 設定に存在することを確認しているだけでした。--no-fast-forward のようなフラグ前置の変種が実際にすり抜けるかどうかは、一切検証していなかったのです。
テストが緑なら、人は「この経路は守られている」と信じます。ところがテストが見ているのは「ルールが書いてある」ことであって、「ルールが意図した攻撃をすべて止める」ことではありません。この差は決定的です。存在確認のテストは、バイパスを検出できないどころか、バイパスがあることを覆い隠してしまうからです。
これは「exit code 0 = 正常動作ではない」と同じ構図です。テストが通った=守れている、ではありません。テストが何を検証しているかを見ないと、緑のテストがかえって油断を生みます。
学び:文字列一致のdenyは「うっかり防止」の1層にすぎない
このケースを普遍化すると、deny ルールの位置づけがはっきりします。文字列一致ベースの deny は、敵対者を止める主防御ではなく、「うっかり事故」を防ぐ多層防御の1層にすぎない、ということです。
substring / prefix マッチは、次の2つを構造的に網羅できません。
- 引数の語順依存:フラグの位置を変えるだけで前方一致が外れる
- 複数の到達経路:REST / GraphQL など、同じ操作の別バリアントを捕捉できない
だから、deny ルールに「これで完全に塞いだ」という期待を載せるのは危険です。deny は「普段の操作でうっかり危険コマンドを打つのを止める」程度の1層と割り切り、本当の防御は HITL(人間の承認)・コードレビュー・CI ゲートといった別の層に置くべきです。
| 防御の層 | 性質 | 止められるもの |
|---|---|---|
| 文字列一致 deny | 1層・語順/経路に弱い | うっかりの危険コマンド |
| バイパステスト(mutation test) | deny の穴を可視化 | 「守れている」という誤認 |
| コードレビュー / CI ゲート | 意味的な検査 | 意図的・変種を含む危険操作 |
| HITL(人間の承認) | 最終防衛 | 上記をすり抜けた全て |
運用Tips:denyには必ずバイパステストを併設する
実務に落とすと、deny ルールを書くときの習慣は次のようになります。
- deny ルールを足したら、「このルールを意図的にすり抜ける入力」をテストとして書く。フラグ前置(
--flag target)、別バリアント(GraphQL など)、別経路を、実際に拒否されるかで検証する。「ルールが存在するか」では不十分。 - すり抜けが見つかったら、塞げるものは塞ぎ、塞げないものは「既知の穴」として明記する。テストやドキュメントに「この変種は現状すり抜ける」と書いておくだけで、誤った安心感は消えます。沈黙が一番危険です。
- deny を「主防御」と思わない。多層防御の1層と位置づけ、レビューと人間の承認を主防御に据える。AI エージェントにガードレールを与えるときも同じで、文字列ブロックは保険、最終的な歯止めは承認フローに持たせます。
1## deny ルールを追加するときのチェック2
31. 正常系(禁止したい操作)が拒否されることを確認42. バイパス変種も拒否されることを確認:5 - フラグを前置した語順(--flag target)6 - 別の API バリアント(GraphQL / 別エンドポイント)7 - 別経路(CLI / 直叩き / 別ツール)83. 塞げない変種は「既知の穴」としてテスト/Docに明記94. deny は1層。主防御はレビュー + 人間の承認に置くまとめ:守るべきは「操作」であって「文字列」ではない
危険な操作を文字列一致で止めるのは手軽ですが、文字列一致が見ているのはコマンドの表記であって、操作の意図ではありません。だから、表記をちょっと変えるだけ(フラグ前置・別バリアント)で、同じ意図の操作がすり抜けます。
今回の学びをまとめます。
- 文字列一致の deny は、引数の語順と複数経路に構造的に弱い
- テストが「ルールの存在」しか確認しないと、誤った安心感を生む
- deny ルールには必ずバイパステストを併設し、すり抜ける変種を可視化する
- 文字列一致の deny は多層防御の1層。主防御はレビューと人間の承認に置く
「ルールがあるから守れている」ではなく、「その操作の全表記・全経路を止められているか」を問う。文字列一致の deny を過信せず、バイパステストで穴を見える化しておくことが、静かなすり抜けを防ぐ習慣です。