| name | adversarial-audit |
| description | 外部から状態を復元できる入口(URL/クエリ、設定ファイル、API 引数、CLI 引数、環境変数、localStorage 等の永続ストレージ)を格子状に地図化し、並列レーンで仮説を形成 → 独立した反証で潰す → 生き残りだけを実機確認 → 常時確認が必要なものをユニットテストへ格上げする監査工程。「攻撃面を洗い出したい」「この入口から何ができるか網羅的に調べたい」「入力の堅牢性を監査したい」「仮説を立てて反証したい」「見つかった不変条件をテストにしたい」といった依頼で必ず使う。単発のバグ調査やコードレビューではなく、入口の集合に対して網羅性と確度の両方を要求されるときの工程。セキュリティに見えて実際は状態整合性の問題であることが多いので、脅威モデルが曖昧なままの「堅牢性監査」でも使う。 |
Adversarial Audit
外部入力から状態を復元できる入口の集合を監査し、確度の高い発見だけを残してリグレッション検知まで持っていく工程。
普通のレビューと違うのは反証を独立した工程として持つ点。実測ではこの工程が発見の 2/3 以上を落とす。落ちたものの多くは「コードの差分は実在するが、実在する差分と実在する不具合は別物」という型だった。反証工程が無ければ、壊れていないものを「修正」することになる。
中心となる考え方
広く見ることと、確かに言うことは、別々に達成する。
網羅性を上げると精度が落ちる。よくある対処は「事前に的を絞る」ことだが、それは視野狭窄を招き、狭めた外側にある本命を落とす。実測でも、最も価値のあった発見(2 つの URL パラメータの相互作用で進行が不可逆に停止する)は、格子が「入口 × 失敗クラス」の空白セルを明示的に担当させたから出た。的を絞っていたら出ていない。
だから精度は範囲を狭めて買うのではなく、主張 1 件あたりの検証を義務づけて買う。これは範囲を 1 ミリも狭めない。しかも安い — 後述のとおり、これが最大のトークン削減にもなる。
いつ使うか / 使わないか
使う: 入口が複数あり、網羅性が要る。発見を信頼して修正やテストに繋げたい。過去に同型の問題が別の場所で再発した履歴がある。
使わない: 特定のバグ 1 件の調査(普通に読めばいい)。入口が 1 つしかない。探索より先に修正方針が決まっている。
Phase 0 — 面を決める
脅威モデルを 1 行で書く
最初に何から何を守るのかを 1 行で書く。これを飛ばすと配分を誤る。
実測では、完全にクライアント側で完結する単独プレイのアプリを「攻撃面」として監査したが、越えられる権限境界が存在しなかった。しかも同じアプリが「進行を一括で書き換えるボタン」を本番ビルドに同梱していた。その状況で「URL でメモリを枯渇させられるか」を複数レーンで追うのは配分ミスである。
境界が無いなら正直に名前を変える。「攻撃面の地図」ではなく「状態整合性と堅牢性の地図」。名前が変われば、追う対象も自然に変わる。
層モデルを書く
入力がシンクに届くまでに通る層を列挙する。弱点は「どの層が欠けているか」で決まる。汎用の型:
| 層 | 内容 |
|---|
| L0 配信 | ヘッダ・キャッシュ・上限(プロキシやエッジの制限を含む) |
| L1 ルーティング | どの入口がどのハンドラへ届くか |
| L2 パース | 切り出し・デコード |
| L3 値の検証 | 定義域(範囲・列挙・カタログ照合) |
| L4 量の検証 | 個数・長さ・合計の上限 |
| L5 権限ゲート | 許可の判定 |
| L6 シンク | 計算量 / メモリ / 永続化 / 描画 / 出力の書き戻し |
L3 と L4 は別物である。「各要素が妥当か」を見ていると「妥当な要素が何個あるか」の門が無いことに気づけない。値の定義域が有限なら個数の上限もそこから導ける。
パース層が 2 系統以上あるかを必ず確認する。 フレームワーク任せの経路と自前実装の経路が混在していると、片方だけに層が欠ける。実測ではこの非対称が実際の穴を生んでいた。
格子を作る
縦軸 = 失敗クラス、横軸 = 入口。汎用の失敗クラス:
| # | クラス | 症状 |
|---|
| A | 全域性の破れ | 不正入力で例外 → 処理停止 |
| B | 量の増幅 | 資源が入力長・入力値に比例 |
| C | 資源の蓄積 | 1 回は安全でも、同じ入口を踏むたびに積む |
| D | 定義域外の値 | 範囲・列挙の検証漏れ(数値文字列の受理など) |
| E | ゲート迂回 | 権限・順序の門を入力で飛ばす |
| F | 永続状態の汚染 | 入力由来の値が永続層へ/壊れた永続値を読む側 |
| G | 出力側の汚染 | 不正な状態から正規化されない出力を書く |
| H | 配信層 | ヘッダ・キャッシュ・フォールバック |
各セルを ✅(検証済み)/ ⚠️(検証はあるが格子として未確認)/ ❓(未検証)で埋める。❓ と ⚠️ のセルだけが Phase 1 の対象。
並列の単位は格子のセル。 入口単位に切ると同型の穴を横断で洗えない(同じ検証漏れが別の入口で再発するのは実際によくある)。クラス単位に切ると、そのクラスが刺さらない入口に時間を使う。
レーン数は空白セルの数で決める。 固定の本数を先に決めない。
Phase 1 — 並列で仮説形成(読むだけ)
各レーンに空白セル群を割り当てる。プロンプトの雛形は references/lane-prompts.md。
報告フォーマット(これが精度を決める)
## H-<レーン>-<n>: <一行の主張>
- 分類: 失敗クラス
- 具体的な入力: <実際に構成できる形>
- 前提となる状態: <必要な事前状態。無ければ「無し」と明記>
- 経路: <file:line> → <file:line> → <到達シンク>
- 観測可能な指標: <何を測れば真偽が決まるか>
- 予測値: <その指標がいくつになると主張するのか>
- 数値の出所: 定数由来 / 実測 / 外挿 のいずれか(必須)
- 既存テストで固定されているか: <テスト名 or 「無い」>
- 反証されるとしたら: <自分の主張が偽になる最も可能性の高い理由>
- 自信度: 高 / 中 / 低
「危なそう」は成果物として不可。 指標と予測値が書けない主張は仮説ではない。
数値の出所ラベルは効く。 実測では、このラベルを義務づけた瞬間にレーン自身が「この 950MB は外挿にすぎない」「この 64 倍は定数から導ける」と分別した。外挿と分かれば深追いしなくて済む。
「前提となる状態」は必ず埋めさせる。 入力だけで作れない仮説は反証工程の再現視点でほぼ落ちる。先に書かせれば安い。
自己検証を義務づける(最重要)
引用する file:line を実際に開いて確認してから報告することをレーンに課す。
これを課さなかった実測では、7 件の誤りが成果物に入った。ファイルの誤同定、件数の誤り、分類の誤り、存在しない経路の主張。どれも「読めば分かる」類である。誤りを含む地図は、地図が無いより悪い — 信頼される場所に置かれるからだ。
そしてこれは範囲を狭めない。同じセルを同じ広さで見たまま、報告の質だけが上がる。
「該当なし」も成果物として要求する
「調べたが仮説にならなかった項目とその理由」を必ず書かせる。これが格子の ❓ を閉じる。要求しないとレーンは肯定的発見しか報告せず、地図が永久に収束しない。実測ではこの節が最も再利用価値が高かった。
不変条件の列挙を要求する
仮説が 0 件でも、そのセルが暗黙に前提としている不変条件を列挙させる。Phase 4 のテスト格上げの原資は、仮説そのものよりこの列挙である。
ゲート
指標と予測値が揃わない主張はレーンへ差し戻す。Phase 2 へ送らない。
Phase 2 — 反証(読むだけ、独立した担当者)
役割は潰すこと。既定は「反証された」。 生き残るには反証側が根拠を出せないことが必要。
作者に反証させない。 「同じ箇所の別の検証が付いていたので目が滑った」という見落としは実在する。作者バイアスそのもの。
4 つの棄却型(どれか 1 つでも成立すれば棄却)
- 上流で落ちる — より手前の層で既に棄却される
- 下流で吸収される — シンクに届いても二重の門が実害を止める。「起きるが誰も困らない」なら実質反証
- 前提が偽 — 仮説が仮定した状態が実際には成立しない
- 既にテストが固定している — 同じ性質を守るテストが存在する
3 つの視点
| 視点 | 問い |
|---|
| 上流 | どの層で死ぬか |
| 下流 | 二重の門が効くか。実害の格はどれくらいか |
| 再現 | その入力・操作列を実際に構成できるか |
再現視点は特に効く。「その URL は長さ制約で構成できない」「そのリンクはアプリ内に存在しない」「その遷移は起きない」で落ちる仮説が多い。
必ず区別させる: 「仕様として意図された挙動」と「見落とし」
設計文書やコードコメントがその挙動を明示的に宣言していないかを確認させる。実測では複数の仮説がこれで死んだ:
- 外挿で出した数値が、設計者が運用点として宣言している数値と一致していた
- 「独立していておかしい」と主張した 2 つの状態が、コメントで "deliberately outside" と断定形で明記されていた
- 「仕様書に記述が無い」と書いたが、実際には記述があった
これは安く、そして致命的に効く確認である。
実行を伴わない工程は同時に走らせられる
Phase 1 と Phase 2 はどちらも読むだけなので、先に出た仮説から順に反証へ流せる。
Phase 2 を飛ばしてよいもの
- 監査者自身がコードで裏を取り終えた確定事項
- レーンが自己反証したもの
- 「情報」レベルで修正判断に影響しないもの
Phase 3 — 実機確認(生き残りのみ)
人に渡すときは 3 点セットで書く
実機確認を依頼者が行う場合、修正 1 件につき次の 3 つを揃える。1 つでも欠けると「直ったのか、元から起きなかったのか」が判別できない。
- 修正前の再現手順 — 旧コードでどう壊れるか。何を見れば壊れていると分かるか
- 修正後の確認手順 — 同じ操作で何が観測されるはずか
- 壊していないことの確認 — その修正が巻き添えにしかねない正常系を 1 つ名指しする
3 番目を忘れやすいが、ここが最も価値がある。「消えるべきものが消えた」だけでなく「消えてはいけないものが残っている」を確認しないと、過剰な修正に気づけない。
新旧の切り替え手段(バージョン管理での退避など)も併記する。
計器は仮説の型で選ぶ
型に合わない計器は空振りする。
| 型 | 計器の例 |
|---|
| B(量) | 割り当てバイトの差分。「入力を N 倍にして差が 0」の形 |
| A / D(全域性・定義域) | プロパティテスト(fuzz + 往復 + 冪等性) |
| E(ゲート) | 述語の同一性テスト(正規経路と入力経路が同じ述語を参照するか) |
| C(蓄積) | 対象の観測可能な状態を読む(解放数・リスナー数・要素数) |
| F / G(永続・出力) | 実環境で書いて読み返す往復 |
| H(配信層) | 実際にリクエストを投げてヘッダ・上限を測る |
観測手段が対象を観測できているかを先に疑う。 実測では、ランタイムが関数参照をキャッシュするために「呼び出し回数」を後付けの差し替えでは捕捉できない、という罠があった。回数ではなく状態を読む。同種の罠は環境ごとにある。
既存のテスト方針と衝突する場合は、別ファイルに置いて例外である理由を冒頭に書く。 「不正な入力を直接投入しない」という原則を持つテスト群がある場合、この監査はその原則の対象外(入力そのものが検証対象)である。黙って混ぜない。
Phase 4 — テストへの格上げ
何を格上げするか
個々のバグの再現ではなく、入口の性質をカタログ全体で測る。
同型の穴は別の場所で再発する。実測でも、ある検証漏れを 1 箇所で直した後、同じ型が別の 2 箇所で再発していた。だから「1 件を直したこと」ではなく「その性質が対象全体で成り立つこと」を固定する。
格上げの優先順位:
- Phase 1 が列挙した不変条件のうち、破れると静かに壊れるもの
- カタログ全体で成り立つ性質(全列挙値・全登録項目に対する全数検査)
- 消去・初期化の網羅性(「消したはずが残る」は静かに壊れる典型)
- 量の上限がどこから導かれるか(実際の門番がどの定数かを杭として固定する)
コメントが参照している存在しないテストを探す
実測で見つかった型: コードコメントが「このテストが実測で守っている」と書いているのに、そのテストが存在しなかった。安く探せて、見つかると確実に価値がある。テスト名らしき文字列を全文検索し、定義が無いものを洗う。
格上げできないものは理由を書いて残す
到達できない(private・UI 層にある・純関数でない)、計器が重すぎる、実行環境が要る、など。理由を書いて残す。書かないと次の横断レビューで同じ仮説が再生産される。
必要ならテスト可能にする小さな改修(定数を到達可能な場所へ切り出す、判定を純関数へ抜く)を先に入れる。これは監査の一部として正当。
見積もりに依存するテストは、そうと明記する
実測値でなく外挿に基づくテストは、定数の位置を守る杭であって実態の測定ではない。doc コメントにそう書く。書かないと、通り続けるのに実態がずれる状態を招く。
期待値の較正
命中率は低い。それが正常。 実測では仮説 34 件のうち修正に至ったのは 4 件(約 12%)。反証工程が 26 件中 20 件を落とした。
この数字を先に共有しておくと、次の 2 つを防げる。
- 「N 件の問題を発見しました」という誤報告。 反証前の仮説は発見ではない。数えてよいのは生き残りだけ。
- 反証工程を「無駄が多い」と誤解して省くこと。 落ちた 20 件は、省いていれば誤った修正になっていた。
修正が製品判断になるものを分離する
生き残った仮説がすべて機械的な修正になるとは限らない。実測では、上限値の決定・機能の存廃・裏口の扱いなど、依頼者が決めるべきものが混ざっていた。
これらは勝手に決めず、選択肢と根拠と推奨を添えて分けて提示する。自分で決めた数値がある場合は、決めたことと根拠を明示する(黙って埋めない)。
トークンを削る(視野を狭めずに)
効いた順:
- Phase 1 に自己検証を義務づける。 最大の削減。Phase 2 が「事実の再確認」ではなく「推論への攻撃」に集中でき、規模を半分以下にできる。範囲は 1 ミリも狭まらない。
- 数値に出所ラベルを義務づける。 外挿と分かれば深追いしない。
- 確定事項は Phase 2 を飛ばす。
- 自己反証・情報レベルは Phase 2 に送らない。
- レーン数を空白セル数で決める。 一律の本数にしない。
- 成果物を確定後に書く。(次項)
やってはいけない削り方: 事前に対象を絞ること。 空白セルを「たぶん安全だから」で外すと、その外側に本命が残る。
成果物の置き方
反証が終わるまで、信頼される場所に確定した体裁で置かない。
実測では反証前に文書をリポジトリへ書き込み、誤った数値を含む文書が仕様のような体裁で数ターン置かれた。訂正はできるが、順序が逆である。作業中は暫定であることを明示するか、確定後に書く。
最終的な文書に含めるもの:
- 層モデルと格子(❓ が閉じた状態)
- 入口ごとの「入口 → パース → 検証 → 到達シンク」
- Phase 1 の結果と確定回答(該当なしと判定した理由)
- Phase 2 の判定表(棄却理由つき)
- 自分の記述に対する訂正(誤りを消さずに残す。次の監査が同じ誤りを再生産しないため)
- 修正した内容と、格上げしたテスト / 格上げできなかったものと理由
失敗モード
実測で起きたもの。
- 前提の思い込みで組み立てた仮説 — 実行順序・所有権・ライフサイクルの思い込みが最も多い。コードの差分が実在することと、不具合が実在することは別。
- 設計者が受容済みのリスクを新発見として報告する — 設計文書の数値と突き合わせれば消える。
- 「壊れている」ではなく「非対称である」を報告する — 同じことを 2 通りで書いている箇所は目を引くが、片方が壊れているとは限らない。
- 地図の枠組みが対象と合っていない — 脅威モデルを 1 行書けば防げる。
- 監査が本題から漂流する — 発見をきっかけに設計改善へ入るのは正しいが、その時点で「監査」ではなくなっていることを自覚し、明示的に切り替える。
サブエージェントが使えない場合
工程は同じだが、レーンを自分で順に回す。その場合反証は必ず別の視点として明示的に回す(同じ思考の流れで続けない)。仮説を書き終えてから、視点を切り替えて 4 つの棄却型を機械的に当てる。作者バイアスは残るので、代わりに「4 つの棄却型を全部当てたか」をチェックリストとして厳密に守る。
参照
references/lane-prompts.md — Phase 1 レーンと Phase 2 反証者のプロンプト雛形。実際に展開するときに読む。