📖 はじめに — セキュリティ負債は「悪意」より「善意の残置」から生まれる
実践編の最終章はセキュリティだ。といっても、派手な脆弱性の話ではない。本番リリース前の総点検で出てきたのは、どれも善意の残置物だった — デバッグに便利だからログに出していたトークン、いつか使うつもりだった認可メソッド、開発を楽にする CORS の localhost 許可。
AI コーディングはこの種の残置を増幅する。「ログを足して」と言えばトークンごと出力し、「権限チェックの仕組みを」と言えば呼ばれないメソッドを丁寧に実装する。だから対処も実践編で積み上げた型と同じ — 機械ガードで構造的に禁じ、使われないものは消し、実挙動で検証する。
🔑 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 指標が過大だった」に続き、これが実践編で 3 度目の検証の真正性問題だ。ローカルの緑は、実行系(フレームワーク・ビルドパイプライン・実 DOM)の緑と同じではない。セキュリティに関わる検証ほど、実挙動側で裏を取る。
🚧 CI ゲート — 実測 95.4% に対して閾値 90 から始める
仕上げに、これらを守る CI ゲートを常設した。カバレッジ閾値は実測 95.4% に対して 90 で開始 — 5.4 ポイントの余裕は「サボり」ではなく、無関係な変更での誤検知からゲートの信頼を守るための設計だ(第7章の ratchet と同じ判断で、段階的に引き上げる)。
🧾 まとめ — そして応用編へ
- 秘匿情報のログ混入は、レビュー観点でなく arch-test の grep 検査で構造禁止する(許容はハッシュのみ)
- 呼ばれない認可コードは攻撃面。YAGNI で削除し、削除の波及は全ソリューション機械確認、復活の条件まで決定ログに残す
- 影響調査は絞った時点で漏れる。全体テストをゲートにし、レビュアーの SHOULD を反証してから閉じる
- .NET 10 の record 検証属性は param ターゲット。
[property:]は 400。ローカル Validator の緑は実挙動を保証しない — 検証は実行系で裏を取る
これで実践編・全 8 ドメインの実録は完結だ。振り返れば、どの章も同じ 4 つの型(default-deny + 全量列挙 / ゼロベースラインが兄弟負債を炙り出す / 定量起票 / 実態調査が過剰設計を殺す)の変奏だった。そして繰り返し現れた「機械で検出できない残余」と「検証自体の真正性」という 2 つの宿題が、そのまま応用編: クロスファミリー検証の出発点になる — 機械の網の外を、実装役とは異なるファミリーの AI がレビューし、そのレビューの実行証跡まで機械で強制する世界へ。
📚 シリーズ記事(ステアリング駆動開発・実践編)
序章
ドメイン別の実録(全 9 章)
- リファクタリング総点検ガイド — 7 つの観点と進め方
- DDD/SOLID/BC 編 — god class 一掃と境界の機械ガード
- SAGA 編 — 新アーキテクチャ挑戦と再発ゲート
- Atomic Design リファクタ編 — 47 page 新規移植
- Storybook × a11y 実機編
- テスト品質・網羅性編
- Python ジョブ群編
- パフォーマンス編
- セキュリティ編
続編
関連記事
- BtoB SaaS セキュリティ設計総覧 — 認証・認可アーキテクチャの全体像
- ゼロトラスト — ID 境界の設計思想