📖 はじめに — セキュリティ負債は「悪意」より「善意の残置」から生まれる
第I部の最終章はセキュリティだ。といっても、派手な脆弱性の話ではない。本番リリース前の総点検で出てきたのは、どれも善意の残置物だった — デバッグに便利だからログに出していたトークン、いつか使うつもりだった認可メソッド、開発を楽にする CORS の localhost 許可。
AI コーディングはこの種の残置を増幅する。「ログを足して」と言えばトークンごと出力し、「権限チェックの仕組みを」と言えば呼ばれないメソッドを丁寧に実装する。だから対処も第I部で積み上げた型と同じ — 機械ガードで構造的に禁じ、使われないものは消し、実挙動で検証する。
🔑 SEC-001: トークンをログに書けなくする — レビューでなく arch-test で
外部モール連携のトークンが、ログ出力に平文で乗る箇所が 3 つあった。修正自体は数分だ。問題は再発防止である。
- 却下案: モックの呼び出し検証(Moq の Verify)でログ出力をテストする — ログ実装の書き方が変わると壊れる脆いテストになる
- 採用案: アーキテクチャテスト内の grep 検査で、「ログ出力テンプレートにトークン変数を渡すパターン」(
Log*(...token={Token})系)をソースレベルで禁止。出してよいのはハッシュ値のみと規約化
「トークンをログに出さない」は人間のレビュー観点としては頼りない — 差分の 1 行に紛れたら見逃す。ソースを走査する機械ガードなら、書いた瞬間に CI が落ちる。第3章の越境テスト、第8章の except-pass ガードと同じ思想の、セキュリティ版である。
🗑️ SEC-002: 呼ばれない認可コードは「資産」ではなく「攻撃面」— YAGNI で消す
認可まわりの点検で、HasPermission という細粒度権限チェックの実装が見つかった。丁寧に作られている。ただし呼び出し箇所はゼロだった。
「いつか使うから残す」が普通の判断に見える。だがセキュリティの文脈では逆だ。呼ばれない認可コードは、(1) 将来誰かが「これを呼べば守られている」と誤解する温床であり、(2) テスト・保守コストを払い続ける死荷重であり、(3) 現行の多層の認可機構で要件は満たされており、この細粒度チェックを必要とする業務要件が現時点で存在しない。判断は YAGNI で削除。
削除の作法が実践編らしい。interface・実装・テストの 3 点セットを消した上で、全ソリューションを警告をエラー扱いにしてビルドし、参照 14 ファイルへの波及を機械確認。さらに「テナント内ロール別の操作制御という業務要件が確定したら、ADR 追記を起点に専用ステアリングで再設計する」という YAGNI の解除条件まで決定ログに残した。消すことと、消した理由が将来覆る条件を、セットで記録する。
🌐 SEC-003: CORS の訂正劇 — レビュアーの SHOULD が的中した
開発用に許可していた localhost オリジンを、本番だけ閉じる — 単純に見える変更が、本章で一番の教材になった。
まず設計。CORS オリジンの組み立てを 純粋関数 BuildCorsOrigins(isDevelopment, configuredOrigins) に切り出した。Web ホストを起動する重い統合テストではなく、DB 不要のユニットテスト 5 件(bool 両側・null 系・コレクション網羅)で仕様を固定し、アプリ本体はこの関数を呼ぶだけにする。テスタビリティのための関数抽出は、セキュリティ設定にこそ効く。
次に事件。当初の判定は IsDevelopment() 単独だった。するとレビューで design-reviewer が SHOULD 指摘を残す — 「Testing 環境で localhost を許可するかどうかが、統合テストの挙動に影響するのでは」。Phase 1 の影響調査では CORS 関連テストだけを見て「問題なし」と判断した。
そして全体テストで、ミドルウェア例外系の統合テストが FAIL した。そのテストは Testing 環境で localhost オリジンを送り、エラー応答にも CORS ヘッダーが付くことを検証していた — IsDevelopment() 単独では Testing が本番扱いになり、ヘッダーが消えたのだ。判定は IsDevelopment() || Testing に訂正された(本番だけ localhost 不許可、という SEC-003 の本質は不変)。
教訓は 2 つ。影響調査は「関連しそうなテスト」に絞った時点で漏れる。単一テストプロジェクトの確認では別プロジェクトの追従漏れを見逃す、という過去の教訓と同型で、だから全体テストをゲートにするルールが効いた。そしてレビュアーの SHOULD(弱い指摘)にも、的中する SHOULD がある。弱い指摘を「対応不要」で閉じる前に、反証(この場合は全体テスト)を通す価値はある。
💣 DEC-010: ローカルの Validator 緑は、実挙動を保証しない
本章最大の泥沼が、入力検証の属性付与だった。.NET の record 型(primary constructor)に [Required] や [StringLength] を付ける際、当初は [property:] ターゲットを明示する方針を採った。ローカルでは Validator.TryValidateObject による軽量テストが緑。設計としても綺麗に見えた。
ところが全体テストで、該当 API が一律 400 を返した。
原因は .NET 10 の実挙動にある。ASP.NET Core のモデルバインディングは、record の primary constructor パラメータへの検証メタデータを param ターゲットで要求する。[property:] を付けると「メタデータがコンストラクタパラメータと一致しない」として、検証以前に 400 Bad Request を返すのだ。方針は param ターゲット直付けに全面訂正した。
さらに皮肉なことに、テスト方式も逆転する。Validator.TryValidateObject は param ターゲットの属性を検出しない。つまり「ローカルで手軽に緑にできるテスト」と「実際のフレームワークが見るメタデータ」が完全にすれ違っていた。回帰防止はコンストラクタパラメータのリフレクションで属性と境界値を検証し、実際の 400 挙動は Controller 統合テストで担保する二段構えに落ち着いた。
第6章の「無スタイル DOM を検査していた」、第7章の「Lines 指標が過大だった」に続き、これが第I部で 3 度目の検証の真正性問題だ。ローカルの緑は、実行系(フレームワーク・ビルドパイプライン・実 DOM)の緑と同じではない。セキュリティに関わる検証ほど、実挙動側で裏を取る。
🚧 CI ゲート — 実測 95.4% に対して閾値 90 から始める
仕上げに、これらを守る CI ゲートを常設した。カバレッジ閾値は実測 95.4% に対して 90 で開始 — 5.4 ポイントの余裕は「サボり」ではなく、無関係な変更での誤検知からゲートの信頼を守るための設計だ(第7章の ratchet と同じ判断で、段階的に引き上げる)。
🧾 まとめ — そして第II部へ
- 秘匿情報のログ混入は、レビュー観点でなく arch-test の grep 検査で構造禁止する(許容はハッシュのみ)
- 呼ばれない認可コードは攻撃面。YAGNI で削除し、削除の波及は全ソリューション機械確認、復活の条件まで決定ログに残す
- 影響調査は絞った時点で漏れる。全体テストをゲートにし、レビュアーの SHOULD を反証してから閉じる
- .NET 10 の record 検証属性は param ターゲット。
[property:]は 400。ローカル Validator の緑は実挙動を保証しない — 検証は実行系で裏を取る
これで第I部・全 8 ドメインの実録は完結だ。振り返れば、どの章も同じ 4 つの型(default-deny + 全量列挙 / ゼロベースラインが兄弟負債を炙り出す / 定量起票 / 実態調査が過剰設計を殺す)の変奏だった。そして繰り返し現れた「機械で検出できない残余」と「検証自体の真正性」という 2 つの宿題が、そのまま第II部: クロスファミリー検証の出発点になる — 機械の網の外を、実装と相関しない別ファミリーの AI がレビューし、そのレビューの実行証跡まで機械で強制する世界へ。
📚 シリーズ記事(ステアリング駆動開発・実践編)
序章
第I部: 技術的負債ドメイン別の実録(総点検ガイド + 8 ドメイン・全 9 回)
- リファクタリング総点検ガイド — 7 つの観点と進め方
- DDD/SOLID/BC 編 — god class 一掃と境界の機械ガード
- SAGA 編 — 新アーキテクチャ挑戦と再発ゲート
- Atomic Design リファクタ編 — 47 page 新規移植
- Storybook × a11y 実機編
- テスト品質・網羅性編
- Python ジョブ群編
- パフォーマンス編
- セキュリティ編(本記事)
第II部: 検証エンジン(クロスファミリー検証・全 6 回)
- 総論 — マルチ LLM の 2 系統と見取り図
- 裏取り編 — +18.1pt 論文の検証
- 設計編 — finder/verifier 分業と逆順禁止
- TDD×ハーネス編 — テスト保護と三層ゲート
- 実装編 — Claude Code の中から Codex を動かす
- モデル戦略編 — ティア割当と「買うか組むか」
終章
- クロスファミリー検証の最終型 —「正しく作る」から「自分で正しさを測り直す」へ — 検証エンジンに「計測」の層をはめ、自己修正ループを閉じる
番外編
- CI テスト 29 分 → 5.6 分 — 検証エンジンの平時運用実録 — 実測が有力仮説を殺し、退行前より速くなった 1 日
- GPT-5.6 sol 切替の当日実録 — もう一つのデフォルト追従 — CLI 更新が黙って替える finder と、当日中の再計測
- 遊休 90% の枠に仕事を振る — Codex 上流調査と verifier バッチ化 — 逆順禁止の境界を ADR で確定し、読む仕事を遊休枠へ移した 1 日
- ハーネスが単一プロジェクトを卒業する — プラグイン化と二層配布 — 4 プロジェクト展開への切り出しと、version + SHA ピンによる非強制追従
- 69分で5リリース — Opus 5 当日対応が暴いた共通ハーネスの死角 — 新モデル対応を起点に、配布・文書・CI の回帰を二消費者で検出した 69 分
関連記事
- BtoB SaaS セキュリティ設計総覧 — 認証・認可アーキテクチャの全体像
- ゼロトラスト — ID 境界の設計思想