技術 約7分で読めます

AI生成APIに潜むIDOR問題とCursorが見逃す3パターン

いけさん目次

NVIDIA NIMの無料OpenAI互換APIを調べたときは、OpenClawやCursorからモデルをどう叩くかが焦点だった。
今回はその続きというより、逆側の話になる。AIエディタにAPIを生成させたとき、そのAPIが本当にマルチユーザー環境で安全か、という話だ。

DEV Communityに AI-Generated APIs Have an IDOR Problem: 3 Patterns Cursor Misses が出ていた。
主張はかなり単純で、CursorやClaude Codeは authenticateToken のような認証ミドルウェアを入れるのは得意だが、/api/documents/:idid がログインユーザー自身のリソースかどうかを見落としやすい、というもの。

これはIDOR(Insecure Direct Object Reference)と呼ばれる古典的な脆弱性だ。
OWASP API Security Top 10では BOLA(Broken Object Level Authorization) として API1:2023 に置かれている。
「ログイン済みか」ではなく、「そのオブジェクトを操作してよいユーザーか」をサーバー側で確認していない状態を指す。

認証済みなのに他人のデータが読める

元記事の例はこういう形だった。

app.get('/api/documents/:id', authenticateToken, async (req, res) => {
  const doc = await Document.findById(req.params.id);
  res.json(doc);
});

authenticateToken があるので、一見すると保護済みのAPIに見える。
だが req.params.id に別ユーザーのドキュメントIDを入れられたとき、doc.userIdreq.user.id を比べていない。
ログインさえしていれば、IDを推測・列挙して他人のデータを読める。

この手のバグが厄介なのは、テストが通りやすいところだ。
「ログインして自分のドキュメントを取得できる」テストは成功する。
「ログインして他人のドキュメントを取得できない」テストを書いていない限り、生成コードはそれっぽく動く。

flowchart TD
    A["ログインユーザー<br/>user_id = alice"] --> B["GET /api/documents/bob_doc_id"]
    B --> C["authenticateToken は通過"]
    C --> D["Document.findById(id)"]
    D --> E["bob の文書を返す"]

Cursorが落としやすいパターン

元記事が挙げているパターンは3つ。

パターンありがちな生成コード起きること
所有者チェックなしfindById(req.params.id)他ユーザーのレコードを読める
削除APIのスコープ漏れfindByIdAndDelete(req.params.id)他ユーザーの投稿や文書を消せる
関連リソースの絞り込み漏れComment.find({ postId })非公開投稿に紐づくコメントだけ抜ける

3つ目が地味に危ない。
親リソースの post は所有者チェック済みでも、/api/posts/:id/comments のような子リソース取得で postId だけ見ていると、そこから情報が漏れる。

app.get('/api/posts/:id/comments', authenticateToken, async (req, res) => {
  const comments = await Comment.find({ postId: req.params.id });
  res.json(comments);
});

公開ブログなら問題にならないこともある。
だが private notes、社内ドキュメント、請求書、チャット、プロジェクト管理ツールでは話が変わる。
「コメントは本文より軽い情報だから大丈夫」ではなく、コメント一覧にも投稿者名、タイムスタンプ、社内文脈、添付IDが出る。

IDORはCWE-862だけでは見落としやすい

元記事では CWE-862(Missing Authorization) として説明されている。
MITREのCWE-862は、リソースや操作にアクセスしようとした主体に対して認可チェックをしていない弱点だ。

ただ、APIレビューでは CWE-862 だけを見るより、OWASPの BOLA として見たほうが刺さりやすい。
BOLAは「APIが受け取ったオブジェクトIDを信じてしまう」問題に焦点を絞っている。
パスパラメータ、クエリ、リクエストボディ、ヘッダに入ったIDを使ってDBを引くAPIは全部対象になる。

観点見るもの
認証リクエストの送信者が誰か
機能単位の認可そのユーザーがAPI機能を呼べるか
オブジェクト単位の認可そのユーザーがこの1件を読んでよいか
プロパティ単位の認可そのユーザーにこのフィールドまで見せてよいか

AIエディタが補完しやすいのは上2つだ。
authenticateTokenrequireAdminrequireRole("manager") みたいなミドルウェアはコードとして分かりやすい。
一方で「この invoice_id が今のテナントに属するか」はドメインモデルを読まないと分からない。

直すならDBクエリに所有権を入れる

元記事の修正方針は「取得してからチェック」ではなく、「DBクエリ時点で所有権を絞る」ことだ。

const doc = await Document.findOne({
  _id: req.params.id,
  userId: req.user.id,
});

if (!doc) {
  return res.status(404).json({ error: 'Not found' });
}

これなら別ユーザーのIDを渡されても、DBから何も返らない。
404を返せば、そのIDが存在するかどうかも漏らしにくい。
403を返す設計もあり得るが、「そのIDは存在するがあなたには権限がない」と教えてしまう場面がある。

削除も同じ考え方に寄せる。

const deleted = await Post.findOneAndDelete({
  _id: req.params.id,
  userId: req.user.id,
});

if (!deleted) {
  return res.status(404).json({ error: 'Not found' });
}

関連リソースの場合は、親の所有者チェックを先に挟むか、JOIN/lookup込みでテナント・所有者を絞る。
MongoDBなら postIdownerId を同じコレクションに持たせる、RDBなら posts.user_id = current_user.id をJOIN条件に入れる。
「コメントは postId だけで引く」をやめるのが要点だ。

AIエディタに渡すルールは短くていい

元記事では、CursorやClaude Codeのルールファイルに「findById ではなく所有者を含めた findOne を使え」と明示することを勧めている。
抽象的な「安全にして」よりも、禁止パターンと生成パターンを具体的に渡したほうがAIエディタは従いやすい。

たとえばNode/Express/Mongooseなら、ルールはこれくらいで足りる。

API route that reads, updates, deletes, or lists user-owned resources must scope database queries by the current user or tenant.

Do not use findById(req.params.id) for user-owned resources.
Use findOne({ _id: req.params.id, userId: req.user.id }) or tenant-scoped equivalents.
Return 404 when the scoped query returns no record.
Add a negative test where user A cannot access user B's resource.

テナント型SaaSなら userId ではなく tenantIdorganizationId になる。
RBACがあるなら、role だけで終わらせず「このorganization内のこのresourceに対するroleか」を見る。
ここを曖昧にすると、admin という名前のロールが別テナントにも効くような事故が起きる。

モデルを変えてもIDORは消えない

IDORはモデルやエンドポイントを差し替えても消えない。
ClaudeでもCursor ComposerでもGPT系でも、生成されたAPIが findById で他人のレコードを返すなら同じ脆弱性になる。
Cursor 3のようにエージェントがIDEの中心に来るほど、「生成されたコードが動く」ことと「権限境界が正しい」ことの差を人間がレビューする場面が増える。

MCPサーバー50件のセキュリティスキャンで見た入力バリデーション欠如とも構造は近い。
AIが書いた薄いAPIラッパーやツールサーバーは、正常系のデモを最短で通すコードになりやすい。
マルチユーザー、マルチテナント、破壊的操作、非公開リソースの4つが絡むところだけは、生成直後に人間が別枠で読む。

チェックする場所は多くない。

APIの形見る条件
GET /resources/:id_id だけでなく userId / tenantId で絞っているか
PATCH /resources/:id更新対象を所有者スコープ内で取っているか
DELETE /resources/:id削除条件に所有者スコープが入っているか
GET /parents/:id/children子リソース取得前に親の所有者を確認しているか
一覧APIfind({}) になっていないか

AIエディタに任せるなら、最後に「user A cannot read/update/delete user B’s resource」のテストを必ず追加させる。
この1本がないAPIは、認証ミドルウェアがどれだけ綺麗でもまだ信用しない。