はじめに
PHPで構築したWebアプリケーションのリリース前に、Claude Codeを使ってセキュリティ監査を実施しました。
「1ラウンド5件ずつ見つけて修正」を繰り返すスタイルで、合計19ラウンド・117件の脆弱性を発見・修正しました。最終ラウンドで新規発見ゼロになるまで続けた、その全過程を記録します。
コードの具体的な内容は伏せますが、「どんな種類の問題が見つかるのか」「なぜ開発者自身が気づけなかったのか」「Claude Codeがどうやって発見したのか」を中心にお伝えします。
なぜClaude Codeでセキュリティ監査をしようと思ったか
このアプリはClaude Code自身が設計・実装したものです。つまり「AIが書いたコードをAIが監査する」という構図です。
一見矛盾しているように思えますが、実際にやってみると非常に有効でした。理由は明確で、「コードを書くとき」と「コードを監査するとき」では視点がまったく異なるからです。
開発中はどうしても「動くこと」に意識が向きます。正常系のフローが通ることを確認して次の機能に移る。一方、監査では「壊すこと」に意識を向けます。異常系、境界値、同時実行、悪意あるリクエスト。この視点の違いが、同じAIでも別の結果を生む理由です。
監査の進め方
基本フロー
各ラウンドは以下の手順で進めました。
- Claude Codeの
code-reviewerエージェントにコードベース全体をスキャンさせる - 発見された問題を重大度別に分類(CRITICAL / HIGH / IMPORTANT / MEDIUM)
- 1ラウンドあたり4〜8件を修正
php -lで構文チェック- 本番サーバーにデプロイして動作確認
- 次のラウンドへ
ラウンド構成
全19ラウンドの内訳です。
| ラウンド | 主な発見カテゴリ | 修正件数 |
|---------|----------------|---------|
| 1-3 | SQLインジェクション、XSS、CSRF不備 | 18件 |
| 4-6 | 認可チェック漏れ、ファイルアップロード脆弱性 | 16件 |
| 7-9 | セッション管理、ディレクトリトラバーサル | 15件 |
| 10-12 | レート制限不備、情報漏洩 | 14件 |
| 13-15 | レースコンディション、TOCTOU | 16件 |
| 16-18 | ビジネスロジック脆弱性、入力検証不備 | 22件 |
| 19 | 最終スキャン(新規発見ゼロ) | 0件 |
後半のラウンドほど「一見正常に動くが、特定条件で破綻する」タイプの問題が多くなりました。
発見された脆弱性の分類
1. レースコンディション(TOCTOU)— 最多カテゴリ
Time-of-Check to Time-of-Use(確認時点と使用時点のズレ)が最も多く見つかりました。
典型的なパターンはこうです。
1. DBから「重複がないか」をSELECTでチェック
2. チェックOKなのでINSERTを実行
この2ステップの間に別のリクエストが割り込むと、重複レコードが作成されます。金銭に関わる処理(成果報酬の記録、請求書生成、振込データ作成)でこのパターンが繰り返し見つかりました。
なぜ気づけなかったか: 開発中は1人でテストするため、同時リクエストが発生しません。テストでは100%正しく動くのに、本番で複数ユーザーが同時にアクセスすると壊れる。これがレースコンディションの厄介なところです。
対策: トランザクション内で SELECT ... FOR UPDATE を使い、チェックとINSERTをアトミックに実行するよう修正しました。
2. 金額計算のバグ — 最も危険だったカテゴリ
報酬計算の関数で、報酬タイプ(固定額 vs パーセンテージ)の分岐が漏れていました。
パーセンテージ型の報酬を設定しても、固定額として処理されてしまう。たとえば「成果の10%」と設定した案件で1万円の成果が発生すると、報酬が10円(1万円の10%ではなく、設定値の10をそのまま円として計算)になるケースがありました。
また別の関数では、振込額がマイナスになりうる計算順序の問題も見つかりました。手数料を引いた後に max(0, ...) で下限を設定していたのですが、その前に「振込可能か」の判定をしていたため、マイナス金額で「振込可能」と判定されるケースが存在しました。
なぜ気づけなかったか: テストデータが固定額の案件ばかりだったためです。パーセンテージ型をテストしていれば一発で気づくバグでしたが、「動いているから大丈夫」と思い込んでいました。
3. 状態遷移の不備
承認・却下などのステータス変更で、「現在のステータスからその遷移が許可されているか」のチェックが甘い箇所が複数ありました。
たとえば、すでに却下された提携申請を再度承認できてしまう。またはキャンセル済みの請求書を再度発行済みに戻せてしまう。
対策: 許可された遷移を明示的にマップとして定義し、遷移前に検証するパターンに統一しました。
$allowedTransitions = [
'pending' => ['approved', 'rejected'],
'approved' => ['suspended'],
// rejected, suspended は終端状態
];
4. セッション管理の問題
ログイン成功後にセッションIDを再生成しても、CSRFトークンが古いままになっていました。これはセッション固定攻撃への対策が不完全だったことを意味します。
また、ログアウト処理の実行順序が間違っていました。セッション破棄の前にCookie削除をすべきところが逆になっていたため、一部のケースでセッションが完全に破棄されない可能性がありました。
5. 入力検証の抜け穴
URLの検証に FILTER_VALIDATE_URL を使っていましたが、このフィルタは javascript:alert(1) のようなURLも有効と判定します。外部サイトのURLを登録する機能で、XSSにつながる可能性がありました。
対策としてスキーム(https:// または http://)の明示的チェックを追加しました。
6. パストラバーサル
ルーターファイルで、リクエストされたパスに対応するPHPファイルを動的に読み込む処理がありました。内部ディレクトリ(設定ファイルやクレデンシャルを含むディレクトリ)へのアクセス制限がなく、URLを工夫すると内部ファイルにアクセスできる可能性がありました。
7. HTTPセキュリティヘッダーの欠如
以下のヘッダーがすべて未設定でした。
X-Frame-Options: DENY— クリックジャッキング防止X-Content-Type-Options: nosniff— MIMEスニッフィング防止Strict-Transport-Security— HTTPS強制Referrer-Policy— リファラー情報の漏洩防止
これらは1行ずつ追加するだけで対策できるのに、意外と忘れがちです。
8. レート制限の不備
ログイン試行にIPベースのレート制限がありましたが、アカウント単位の制限がありませんでした。つまり、多数のIPアドレスから同一アカウントに対してブルートフォース攻撃ができる状態でした。
IPベースとアカウントベース、両方のレート制限を組み合わせることで対策しました。
なぜ「AIが書いたコード」にこれだけの脆弱性があったのか
率直に分析すると、以下の理由が挙げられます。
1. 機能実装の優先
開発指示が「この機能を作って」という形で来るため、AIは機能を動かすことに集中します。セキュリティは「追加の要件」として意識しないと対処されません。
2. テストの不在
ユニットテストやインテグレーションテストがない環境では、エッジケースの検証が手動になります。レースコンディションのような同時実行の問題は手動では発見できません。
3. 段階的な開発による見落とし
最初にシンプルな機能を作り、後から拡張していく過程で、初期の前提が崩れます。たとえば「報酬は固定額のみ」という前提で書いた計算関数に、後からパーセンテージ型を追加した際に、分岐の追加を忘れる。
4. コピーアンドペーストの罠
似たような処理(チェック→INSERT)をコピーして別の箇所に使い回すと、同じ脆弱性パターンが複数箇所に広がります。実際、TOCTOUの問題は6箇所以上で同じパターンで見つかりました。
Claude Codeはどうやって発見したのか
Claude Codeの code-reviewer エージェントは、以下の観点でコードを分析しています。
パターンマッチング: 「SELECTの後にINSERT」「ステータスチェックなしの更新」といった危険なパターンを認識します。人間のコードレビューでも同じことをしますが、AIは疲れません。全ファイルを同じ集中力でスキャンできます。
文脈理解: 単なる正規表現マッチではなく、コードの意図を理解した上で「この処理は金銭に関わるからアトミックであるべき」といった判断ができます。
網羅性: 人間のレビューでは「この辺は大丈夫だろう」とスキップしがちな補助的なファイル(ヘルパー関数、ルーター、設定ファイル)も漏れなくチェックします。
19ラウンドで学んだこと
セキュリティは層で考える
1つの対策で完璧にはなりません。入力検証、トランザクション、状態遷移ガード、レート制限、HTTPヘッダー。それぞれ異なるレイヤーの防御です。
「動いている」は「安全」ではない
テスト環境で問題なく動作するコードの中に、117件の脆弱性がありました。正常系が動くことと、異常系に耐えることはまったく別の問題です。
AIによる監査は有効、ただし万能ではない
Claude Codeは既知のパターンの脆弱性を効率的に発見できます。ただし、ビジネスロジック固有の問題(「この金額は本当に正しいか」)は、ドメイン知識がないと判断できません。AIの監査は人間のレビューを置き換えるものではなく、補完するものです。
リリース前の監査は必須
今回、リリース前に発見できたことで実害はゼロでした。もしこのまま本番運用していたら、金銭的な損害が発生していた可能性があります。特にレースコンディションと金額計算のバグは、発覚が遅れるほど影響が大きくなる性質のものでした。
再利用可能なセキュリティチェックリスト
この監査を通じて作成した、PHPアプリ向けの簡易チェックリストです。
入力・出力
- 全ユーザー入力にバリデーション(型、長さ、形式)があるか
- HTMLエスケープ(
htmlspecialchars)を徹底しているか - URLバリデーションでスキーム制限をしているか
認証・セッション
- ログイン後にセッションIDを再生成しているか
- CSRFトークンをログイン後に更新しているか
- ログアウトでセッションを完全に破棄しているか
- パスワードは
password_hash/password_verifyを使っているか
データ整合性
- 重複チェックとINSERTがトランザクション内にあるか
- 金銭処理に
FOR UPDATEロックを使っているか - ステータス遷移に明示的な許可マップがあるか
レート制限
- IP単位とアカウント単位の両方のレート制限があるか
- 登録、ログイン、パスワードリセットすべてに適用されているか
HTTPセキュリティ
X-Frame-Options、X-Content-Type-Options、HSTS、Referrer-Policyが設定されているか- CORSの設定でCredentialsを不必要に許可していないか
ファイル・パス
- アップロードファイルのMIMEタイプを検証しているか
- パストラバーサルを防止しているか(
realpathチェック等) - 内部ディレクトリへのHTTPアクセスをブロックしているか
まとめ
19ラウンド・117件。この数字は「AIが書いたコードの品質が低い」ことを示しているのではなく、「セキュリティ監査なしにリリースすることの危険性」を示しています。
どんなに優秀な開発者(人間でもAIでも)が書いたコードでも、セキュリティの専門的な視点でのレビューは必要です。Claude Codeはその作業を効率化する強力なツールですが、重要なのは「監査するという判断」そのものです。
もしClaude Codeで開発しているプロジェクトがあれば、リリース前にcode-reviewerエージェントでの監査を強くおすすめします。1セッションで完了するような規模ではありませんが、リリース後に脆弱性が見つかるよりははるかにマシです。