🔓

自分のコードをAIに攻撃させたら"守り"が全部ザルだった

に公開
4

「セキュリティ、ちゃんとやってる?」

この質問、正直めちゃくちゃ怖い。

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つだった。

  1. IDOR(認可の欠如) — 認証はあるが認可がない
  2. Mass Assignment — リクエストボディをそのままDBに渡している
  3. 情報漏洩 — 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

hibarahibara

細かい指摘ですみません。「参考」にある、「shannon - 自律型AI脆弱性スキャナー」のリンクが「404 not found」ですね。

https://github.com/shannon-ai/ には、レポジトリが2つしかないので、ここにリンクした方が良さそうです。

Rё∀Lβios(アルビオン)Rё∀Lβios(アルビオン)

大変興味深い内容で、参考になりました。
案出し係としてAIが優秀であり、専門学習させた知能を活用すれば、一定のセキュリティ水準に持っていくための担保(Linter)的存在として巧い使い方だなと為になりました。