自分のコードをAIに攻撃させたら"守り"が全部ザルだった
「セキュリティ、ちゃんとやってる?」
この質問、正直めちゃくちゃ怖い。
SQLインジェクション対策? やってる。XSS対策? エスケープしてる。CSRF? トークン入れてる。
——でも、「ちゃんと」って何だ?
OWASPのチェックリストを上から順に潰して、ESLintのセキュリティプラグインを入れて、dependabotのアラートを処理して。やることはやっている。はずだった。
自分のコードに自律型AIハッカーをけしかけるまでは。
はじめに
セキュリティは「そこそこ意識している」つもりだった。
でも今は、コードを書くときの思考回路がまるっきり変わった。守る側の視点だけでコードを書いていた頃には、絶対に気づけなかったことがある。
きっかけは、GitHub Trendingで爆発的に伸びていた「shannon」という自律型AIハッキングツールだった。1日で+3,000スター以上。AIエージェントが自律的にWebアプリの脆弱性を探索し、攻撃を試みるツールだ。
「面白そう」と思って自分のサイドプロジェクトに向けて走らせた。
結果、見えていた世界が完全にひっくり返った。
セキュリティ対策、やってるはずなのに
僕のサイドプロジェクトは、よくあるSaaS系のWebアプリだ。Next.js + Prisma + PostgreSQL。認証はNextAuth。特に変わったことはしていない。
セキュリティ対策も「標準的」にやっていた。
- PrismaのパラメータバインディングでSQLインジェクション対策
- ReactのJSXによる自動エスケープでXSS対策
- CSRFトークンの付与
- Helmet.jsでセキュリティヘッダーを設定
- rate limitをミドルウェアで実装
「教科書通り」だ。
でも、shannonを動かしてみた結果、出てきたレポートを見て血の気が引いた。
見つかった問題の例:
[CRITICAL] APIエンドポイント /api/projects/[id] において、
認証済みユーザーAが、ユーザーBのプロジェクトIDを指定して
GET/PUT/DELETEリクエストを送信した場合、
オーナーシップの検証なしにデータへアクセス・変更が可能。
(IDOR: Insecure Direct Object Reference)
つまり、こういうコードだ。
// 僕が書いていたコード
export async function GET(req: Request, { params }: { params: { id: string } }) {
const session = await getServerSession(authOptions)
if (!session) return new Response("Unauthorized", { status: 401 })
const project = await prisma.project.findUnique({
where: { id: params.id }
})
return Response.json(project)
}
認証はしている。ログインしていないユーザーは弾ける。
——でも、「ログインしているユーザーが、他人のデータにアクセスできない」ことは検証していなかった。
これ、SQLインジェクションでもXSSでもない。OWASPのチェックリストで真っ先に出てくる「Broken Access Control」なのに、僕は完全に見落としていた。
しかもこのパターンが、15個のAPIエンドポイントのうち11個で見つかった。
世間の反応——2つの派閥
セキュリティの話をすると、だいたい2つの派閥に分かれる。
「ツールで自動化すべき」派。
SAST(静的解析)やDAST(動的解析)を導入しろ。CI/CDに脆弱性スキャンを組み込め。Snyk入れろ、SonarQube入れろ。ツールが検出してくれるから人間が頑張る必要はない——という立場。
「セキュリティの知識を身につけるべき」派。
ツールは万能じゃない。開発者がOWASPを理解して、セキュアコーディングのプラクティスを学べ。コードレビューで指摘できるレベルになれ——という立場。
どちらも正論だ。
でも正直、どちらもピンとこなかった。
ツールは入れている。SASTも動かしている。でもIDORは検出されなかった。ビジネスロジックに紐づく脆弱性は、静的解析では見つけられないことが多い。
知識も、一応ある。OWASPのTop 10は知っている。Broken Access Controlが1位なのも知っている。知っていて、なお見落とした。
知識があっても、ツールがあっても、「守る側の視点」だけで書いている限り、穴は生まれ続ける。
なぜか。
防御側の思考は「これを守ろう」だからだ。攻撃側の思考は「どこが甘い?」だからだ。問いの立て方が根本的に違う。
第三の道——「攻撃をAIに委ねる」という発想
僕がたどり着いたのは、「自分のコードを攻撃するAIエージェントを、開発プロセスに組み込む」というアプローチだった。
具体的にはこうだ。
ステップ1: ローカル環境で自律型AIハッカーを走らせる
shannonのようなツールをステージング環境に向けて動かす。本番には絶対に向けない。あくまで自分のコードの、自分の環境で。
ステップ2: レポートを読んで、パターンを分類する
AIが見つけた脆弱性を読むと、あることに気づく。ほとんどが同じパターンの繰り返しだということだ。
僕の場合、圧倒的に多かったのがこの3つだった。
- IDOR(認可の欠如) — 認証はあるが認可がない
- Mass Assignment — リクエストボディをそのままDBに渡している
- 情報漏洩 — APIレスポンスに不要なフィールドが含まれている
ステップ3: パターンごとに「構造的な防御」を入れる
ここが一番の転換点だった。個別のエンドポイントを直すんじゃない。パターンとして潰す。
たとえばIDOR対策は、こう変えた。
// Before: 認証だけ
const project = await prisma.project.findUnique({
where: { id: params.id }
})
// After: 認可を構造に組み込む
const project = await prisma.project.findUnique({
where: {
id: params.id,
userId: session.user.id // オーナーシップをクエリ条件に含める
}
})
たった1行の追加だ。でもこの1行を、15個のエンドポイントすべてに入れる必要があった。
さらに、この「忘れ」が二度と起きないように、ミドルウェアレベルで認可チェックを共通化した。
// 認可ミドルウェア
function withOwnership<T extends { userId: string }>(
handler: (req: Request, resource: T) => Promise<Response>,
findResource: (id: string) => Promise<T | null>
) {
return async (req: Request, { params }: { params: { id: string } }) => {
const session = await getServerSession(authOptions)
if (!session) return new Response("Unauthorized", { status: 401 })
const resource = await findResource(params.id)
if (!resource) return new Response("Not Found", { status: 404 })
if (resource.userId !== session.user.id) {
return new Response("Forbidden", { status: 403 })
}
return handler(req, resource)
}
}
Mass Assignmentは、Zodでリクエストボディを明示的にバリデーションするようにした。
// Before: リクエストボディをそのまま使用
const data = await req.json()
await prisma.project.update({ where: { id }, data })
// After: 許可するフィールドを明示的に定義
const UpdateSchema = z.object({
name: z.string().max(100),
description: z.string().max(1000).optional(),
})
const data = UpdateSchema.parse(await req.json())
await prisma.project.update({ where: { id }, data })
情報漏洩は、レスポンス用のDTO(Data Transfer Object)を定義して、必要なフィールドだけを返すようにした。
どれも難しいことじゃない。教科書に載っていることだ。
——でも、「攻撃された」からこそ、骨身に染みた。
気づき——「攻撃者の目線」がくれたもの
この経験で気づいたことが3つある。
1つ目。セキュリティ対策の大半は「入口」を守っているだけだった。
SQLインジェクション対策、XSS対策、CSRF対策。これらは全部「入口の門番」だ。でも、門を通った後の世界——認証済みユーザーが何をできるか——は、ほぼノーガードだった。
攻撃者はログインする。正規のユーザーとして。そしてURLのIDを1つ変えるだけで、他人のデータを盗む。
この視点は、守る側からは出てこない。
2つ目。AIハッカーは「疲れない」。
人間のペネトレーションテスターなら、15個のエンドポイントを全部手動で試すのは面倒だ。たぶん主要な3〜4個を試して、「ここが通るなら他も同じだろう」と推測する。
AIは違う。全部試す。しかもパラメータの組み合わせを変えながら、延々と。
僕のアプリで見つかった11個のIDOR脆弱性のうち、人間なら「まさかここは大丈夫だろう」と思いそうなマイナーなエンドポイント(/api/projects/[id]/export-history とか)でも、きっちり突いてきた。
3つ目。そして一番大きな気づき。
セキュリティって、「チェックリストを埋める作業」じゃなかった。
「自分のコードを、悪意ある目で見つめ直す行為」だった。
その「悪意ある目」を、今はAIが代行してくれる。しかも、人間より網羅的に、人間より執拗に。
僕たちは長い間、「防御側の視点」だけでセキュリティを考えてきた。壁を高くする、鍵を増やす、門番を置く。
でも本当に必要だったのは、「自分の壁を自分で殴ってみる」ことだった。そしてそれを、AIがめちゃくちゃ上手くやってくれる時代になった。
結論
OWASPを暗記しても、セキュリティプラグインを全部入れても、守る側の思考だけでは穴は塞がらない。
攻撃者の目線を持つこと。それが最強の防御になる。
そして今、その「攻撃者の目線」を手軽に手に入れる方法がある。AIに自分のコードを攻撃させればいい。
もちろん、自律型AIハッカーを扱うには注意が必要だ。必ず自分の環境で、自分のコードにだけ使うこと。他人のシステムに向けたら、それは普通に犯罪だ。
でも、「自分のコードを攻撃する」という行為のハードルが劇的に下がったことは、たぶんセキュリティの民主化と呼んでいいと思う。
あなたのコード、最後に「殴られた」のはいつだろう?
一度、AIに殴らせてみてほしい。思っている以上に、ボコボコにされるから。
Discussion
細かい指摘ですみません。「参考」にある、「shannon - 自律型AI脆弱性スキャナー」のリンクが「404 not found」ですね。
https://github.com/shannon-ai/ には、レポジトリが2つしかないので、ここにリンクした方が良さそうです。
ご指摘ありがとうございます!確認したところ、おっしゃる通り404になっていました。
https://github.com/KeygraphHQ/shannon の正しいリンクに修正しました。
助かりました!
大変興味深い内容で、参考になりました。
案出し係としてAIが優秀であり、専門学習させた知能を活用すれば、一定のセキュリティ水準に持っていくための担保(Linter)的存在として巧い使い方だなと為になりました。
勉強になりました🙏