一番の脆弱性は"人間のコードレビュー"だった
「LGTM 🚀」
このたった4文字、何回書いてきただろう。
PRが来て、差分を見て、ロジックを追って、「まあ問題なさそうだな」でApprove。正直、金曜の夕方に来た30ファイル変更のPRに対して、全行を真剣に読んだかと聞かれたら——答えに詰まる。
たぶん、あなたもそうだと思う。
はじめに
認証・認可、入力バリデーション、そのあたりを「ちゃんとやってるつもり」で何年もやってきた。
でも最近、自律型AIエージェントにセキュリティテストを任せてみたら、僕が一番信頼していた「人間のコードレビュー」が、実は一番のザルだったと気づいた。
ツールを入れて満足する話じゃない。もっと手前の話だった。
AIハッカーの時代が来た、らしい
2025年あたりから、AIを使った脆弱性スキャナーが一気に増えた。
中でも話題になったのが Shannon。GitHubでトレンド入りした自律型AI脆弱性スキャナーで、LLMを使ってコードベースを自律的に解析し、セキュリティホールを見つけてくれる。人間が「ここを見て」と指定しなくても、エージェントが自分で判断して探索する。
Hacker Newsでも「AIでコード品質を上げるには」みたいな議論が盛り上がっていて、Zennでもセキュリティ×AIエージェントの記事がトレンドに上がるようになった。
「これは試さないと」と思った。
僕がやったこと
自分が関わっているプロジェクトのリポジトリに対して、AIエージェントベースのセキュリティスキャンを走らせてみた。
期待していたのは、こういうやつ。
- SQLインジェクションの検出
- XSSの脆弱性
- 認証バイパスの可能性
つまり「人間が見落としがちな、技術的に高度な脆弱性」を見つけてほしかった。
結果は——予想と全然違った。
出てきたのは「高度な脆弱性」じゃなかった
AIが指摘してきたのは、たとえばこういうコードだった。
// ユーザー入力をそのまま使ったリダイレクト
app.get('/callback', (req, res) => {
const redirectUrl = req.query.next || '/dashboard';
res.redirect(redirectUrl);
});
オープンリダイレクト。古典的すぎる。
# デバッグ用に残ったエンドポイント
@app.route('/debug/users')
def debug_users():
return jsonify(User.query.all())
本番に残ったデバッグエンドポイント。初歩的すぎる。
# docker-compose.yml
services:
db:
environment:
POSTGRES_PASSWORD: "postgres"
ports:
- "5432:5432"
デフォルトパスワードのまま外部ポートを公開。基本中の基本。
どれも、「そんなの見ればわかるだろ」というレベルのもの。
でも、全部コードレビューを通っていた。
世間の反応:2つの派閥
この話をすると、だいたい2つの反応に分かれる。
「ツール派」 は言う。
「だからLintやSASTツールをCIに組み込むべき。Semgrep、Snyk、SonarQube。自動化すれば解決する」
正論だと思う。実際、これらのツールを入れていれば上の例は検出できた。
「プロセス派」 は言う。
「レビューのチェックリストを作れ。セキュリティ観点のレビュー項目を標準化すべきだ」
これも正論。チェックリストがあれば、少なくとも「見るべきポイント」は明確になる。
でも、正直、どっちもピンとこなかった。
なぜなら、ツールもチェックリストも、すでにあったからだ。
Semgrepは入っていた。レビュー用のチェックリストもNotion上にあった。それでも、これらの脆弱性はすり抜けていた。
なぜすり抜けたのか
理由を掘り下げていくと、こういう構造が見えてきた。
Semgrepのルールは、あったけど古かった。 カスタムルールを最後に更新したのは半年前。新しいルーティングパターンに対応していなかった。
チェックリストは、あったけど形骸化していた。 「セキュリティ観点を確認」という項目にチェックを入れるだけの儀式になっていた。何を確認したのか、誰も説明できない状態。
そして何より——「このコードは安全だ」という思い込みがあった。
デバッグエンドポイントが残っていたのは、元々feature flagで制御していたから。「本番では動かないはず」と全員が思っていた。でもfeature flagの設定ファイルをよく見ると、本番環境でもdebugモードがオンになるエッジケースがあった。
オープンリダイレクトは、「内部のリダイレクトだからURLバリデーションはいらない」と誰かが判断した。その判断がPRのコメントに残っていた。レビュワーも「たしかに」とApproveしていた。
つまり問題は、ツールの不在でもプロセスの不在でもなく——「まあ大丈夫だろう」を全員で共有していたことだった。
第三の道:AIを「思い込み破壊装置」として使う
僕がたどり着いたのは、AIセキュリティツールを「脆弱性スキャナー」としてではなく、「人間の思い込みを検出する装置」として使う、という発想だった。
具体的にやったこと。
1. AIに「なぜ安全だと思うのか」を質問させる
コードレビューのプロセスに、AIエージェントを組み込んだ。ただし、脆弱性を指摘させるのではなく、レビュワーの判断根拠を問い直す役割を持たせた。
# AIからのレビューコメントの例
「このリダイレクト先のバリデーションが省略されていますが、
省略しても安全である根拠は何ですか?
- 内部URLのみを想定している場合、その制約はどこで保証されていますか?」
これが効いた。「まあ大丈夫だろう」で通していた箇所に、「なぜ大丈夫なのか」を言語化する圧がかかる。
2. 「前提条件」をコメントとして残すルール
AIに指摘された箇所について、「なぜ安全か」の根拠をPRコメントに残すようにした。
// ✅ リダイレクト先は allowList で制限済み(see: src/config/redirects.ts)
const redirectUrl = validateRedirect(req.query.next) || '/dashboard';
res.redirect(redirectUrl);
コードを直すだけじゃなく、「安全である前提条件」を明示する。前提が変わったときに、次のレビュワーが気づけるように。
3. 定期的に「前提条件の棚卸し」をする
月1で、過去のPRコメントに残った「安全である根拠」をAIに再検証させる。
「この前提、今も成り立ってますか?」
feature flagの設定変更、依存ライブラリのアップデート、ルーティング構成の変更——前提が崩れるタイミングは無数にある。人間はその全部を覚えていられないけど、AIは過去のコメントを全部読める。
気づき:一番の脆弱性は「信頼」だった
AIエージェントにセキュリティテストを任せて気づいたのは、皮肉な事実だった。
一番の脆弱性は、技術的な穴じゃなく、「このコードは大丈夫」という人間の信頼そのものだった。
Shannonのような自律型AIスキャナーがすごいのは、ゼロデイを見つける能力じゃない。人間が「見なくていい」と思った場所を、何の先入観もなく見る能力だ。
人間のレビュワーには文脈がある。「このコードは〇〇さんが書いたから大丈夫」「このパターンは前にも通したから大丈夫」「ここは内部APIだから大丈夫」。
その「大丈夫」の蓄積が、セキュリティホールを作っていた。
冒頭の「LGTM 🚀」に戻る。あの4文字の裏にあったのは、レビューの完了ではなく——思考の停止だったのかもしれない。
結論
セキュリティツールを導入する前に、やることがある。
自分のチームの直近のPRを10個開いて、Approveコメントを読み返してみてほしい。「なぜ安全か」が書かれているApproveは、何個あるだろう?
AIエージェントの本当の価値は、脆弱性を見つけることじゃない。「まあ大丈夫だろう」を許さない、空気の読めない同僚になってくれることだ。
セキュリティで一番怖いのは、知らない脅威じゃない。
知ってるのに「大丈夫」と思い込んでいる、自分自身だ。
——あなたの「LGTM」、本当にLooks Goodですか?
Discussion
「AIを脆弱性スキャナーじゃなくて、判断根拠を問い返す役に使う」って発想、すごくよかったです。答えを出させるより問いを立てさせる方が、人間の思考停止に刺さりますよね。
導入時のチームの反応とかも気になりました。続き読みたいです
人間って思い込む動物なので、いつも観てるから大丈夫だろうで盲点になる部分はかならずあると思うんすよね
でも、LLMが出てきたからリスクポイントを文脈ベースで洗い出せるようになってきたと思ってます。
理想的な使い方だなーと思ったっす