Claude(Opus)が実装し、テスト200件超が通ったヘッドレスCMSの差分を、別のAIであるGLM-5.3(Z.ai)に「攻撃者の目で読め」と5回通した。
指摘は延べ55件。Opusが実物のコードで1件ずつ確かめ、20件を採用した。1回目の査読で、他人のアカウントを乗っ取れる重大な穴が見つかっている。
翌日に自社のSEOツールでも同じ手順を回すと、GLMの指摘を受けて足した修正のほうに、新しい穴が開いていた。
前提
事例1: 自社のヘッドレスCMS
- 構成: Cloudflare Workers・D1・R2
- 追加した機能(1日で実装)
- 承認フロー
- ID/パスワードログイン
- 公開終了日時
- 添付ファイル
- 地図/動画の埋め込み
- 実装: すべてClaude(Opus)
- 査読前の状態: テスト200件超が通過済み、ローカルの動作確認も済み
- 査読役: GLM-5.3(Z.ai)
事例2: 自社のSEOツール
- 追加した機能: 関連キーワードの取得、URLの取得
- 査読の対象: 前日の差分とツール全体
- 査読役: GLM-5.3
- 査読の回数: 4回
手順
査読は以下の流れで回した。
1. Claude(Opus)が機能を実装し、テストとローカルの動作確認を通す
2. 差分を GLM-5.3 に渡し、「攻撃者の目で読め」と指示する
3. GLM-5.3 の指摘を Opus が実物のコードで1件ずつ確かめ、採否を決める
4. 採用した指摘だけを直す
5. 直した差分を、もう一度 GLM-5.3 に通す
6. 新しい穴が出なくなるまで 2〜5 を繰り返す
手順3で、GLMの指摘をそのまま正としては扱わない。事例1では55件中35件を捨てている。
GLMは「攻撃者の目で」読むので、実際には到達できない経路や、すでに別の場所で防いでいる入力まで指摘してくる。
採否を実物のコードで確かめる工程を飛ばすと、不要な修正が増え、修正した箇所がまた次の穴の入口になる(事例2で実際に起きた)。
事例1: 査読5回の記録
査読は2026-10-17 23:25から10-18 00:04までの間に5回回した。
| 回 | 何を通したか | 指摘 | 採用 | 判定 |
|---|---|---|---|---|
| 1 | 機能追加の差分(本番に出す直前) | 7 | 5 | 重大 |
| 2 | 査読の門のスクリプト自体 | 7 | 5 | 要確認 |
| 3 | 門の修正 | 6 | 0 | 要確認 |
| 4 | その日の変更全体を4塊に分けて通し直し | 28 | 9 | 重大 |
| 5 | 4回目の修正の差分 | 7 | 1 | 要確認 |
合計は指摘55件・採用20件・不採用35件。
表から読み取れる点は3つある。
- 1回目で「重大」が出た。 テスト200件超が通り、Claudeも問題を挙げていなかった差分だった
- 4回目で再び「重大」が出た。 差分単位ではなく、その日の変更全体を4つの塊に分けて通し直すと、指摘が28件に増えた。差分だけ見ていると、変更どうしの組み合わせで生まれる穴を見落とす
- 5回目でも新しい穴が1件出た。 直した差分を通し直す工程は、1回では終わらない
2回目と3回目は、査読を回すためのスクリプト(門)そのものを査読にかけた回になる。査読の仕組み自体にも穴があり得るので、仕組みも同じ目で読ませた。
事例1で見つかった重大な穴: 他人のアカウントにパスワードを付けて乗っ取れた
1回目の査読でGLMが見つけた穴。
- 場面: サイトのオーナーが、メンバーにIDとパスワードを発行できる機能
- 穴: メンバーを追加するときに、メールの確認が無い。そのため、他人のメールアドレスでメンバーを作り、パスワードを付けることで、そのアカウントを乗っ取れた
「オーナーがメンバーを招待する」という正常系だけを見ると、メールアドレスを入力してパスワードを発行する流れは自然に見える。
テストも正常系の動きを確かめるので、全部通る。
「入力したメールアドレスが、本当にその人のものか」を確かめる工程が無いことは、悪用する側の視点に立たないと気づきにくい。
自治体向けの提案に合わせて1日で5つの機能を足した日だった。ID/パスワードログインを足した日に、ログインまわりで重大な穴が出ている。
事例2: 直した修正のほうに穴が開いた
翌日(2026-10-18)、自社のSEOツールに関連キーワードとURL取得の機能を足し、前日の差分とツール全体をGLM-5.3で4回査読した。
指摘27件をOpusが実物で確かめ、10件を直して本番に出した。
ここで起きたのは、GLMに言われて足した修正のほうに穴が開くという現象だった。
落とし穴1: 回数の上限が同時リクエストで素通りできた
GLMの指摘を受けて、機能の呼び出しに回数の上限を足した。
その上限が、同時にリクエストを送ると素通りできた。
「今の回数を読む → 上限未満なら処理する → 回数を増やす」と工程を分けて書くと、読んでから増やすまでの間に別のリクエストが割り込める。
同時に送られたリクエストは、どれも「まだ上限未満」と読んでしまう。
落とし穴2: 「localhostなら通す」がHostヘッダーで破れた
開発中にログインを省くための「localhostなら通す」という修正が、Hostヘッダーの偽装で破れた。
Hostヘッダーはリクエストを送る側が自由に書ける値なので、本番に対して localhost を名乗るリクエストを送れば、開発用の抜け道に入れてしまう。
対処
回数の上限
上限は1文のSQLで先に予約する形に変えた。
回数を読む処理と増やす処理を1文にまとめ、上限に達していたら予約自体が失敗するようにした。読んでから増やすまでの隙間が無くなるので、同時リクエストでも上限を超えない。
開発用ログイン
開発用ログインは、設定ファイルの明示フラグだけで許す形に変えた。
リクエストに含まれる値(Hostヘッダー)で開発環境かどうかを判定するのをやめ、サーバー側の設定で明示したときだけ開発用ログインが有効になる。
本番でHostを偽装したリクエストを送り、403が返ることを確かめてから出した。
2つの事例から言えること
| 項目 | 事例1(ヘッドレスCMS) | 事例2(SEOツール) |
|---|---|---|
| 査読の回数 | 5回 | 4回 |
| 指摘 | 延べ55件 | 27件 |
| 採用・修正 | 20件 | 10件 |
| 主な穴 | メール確認なしでの乗っ取り | 修正で足した上限・開発用ログインの穴 |
- テストが通ることと、攻撃に耐えることは別物だった。事例1は、テスト200件超が通った状態で重大な穴が見つかった
- 別のAIの指摘は、そのまま正にしない。事例1では55件中35件を捨てた。採否は、書いた側のAIが実物のコードで確かめて決める
- 直した差分は、もう一度通す。事例1では5回目でも新しい穴が出て、事例2では修正そのものが穴になった
- 差分だけでなく、その日の変更全体を塊に分けて通し直す。事例1では、その工程で指摘が28件に増え、重大な穴も見つかった
- 開発環境かどうかの判定に、リクエスト側が書ける値(Hostヘッダー)を使わない。サーバー側の設定で明示する