TL;DR
- AIエディタは、所有権チェックなしでIDによってリソースを取得するルートを生成する――典型的なIDOR(CWE-639)
- このパターンは「雰囲気(vibe)コード化された」アプリに至る所にある:認証済みの任意のユーザーが、他人のデータを読み取れる
- DBクエリに追加の1条件を入れると直る――ただしAIは、そう頼まない限りそれを入れない
先月、サイドプロジェクトをレビューしました。Node/Expressのバックエンドで、Cursor生成、構造はきれいで、コメントも丁寧。開発者は自分の認証設定に誇りを持っていました――JWTトークン、bcryptのパスワード、保護されたルート。ちゃんとした内容です。
ところが /api/orders/1 を叩いたところ、ユーザー847としてログインしているのに、ユーザー1の注文が返ってきました。全部です。名前、住所、アイテム、合計。次に /api/orders/2 に切り替えても同じ結果。APIは認証されていました――到達するには有効なJWTが必要です。ですが、それが誰のJWTかは気にしていませんでした。
これはIDORです。Insecure Direct Object Reference(不十分な直接オブジェクト参照)。OWASPはAPI Security Top 10で#1にランク付けしています。そしてAIエディタは、生成するあらゆるリソースのエンドポイントでこれを再現します。
The Vulnerable Pattern
Cursorが「ユーザーの注文を取得するエンドポイント」をプロンプトすると、こう出力されます:
// CWE-639: Authorization Bypass Through User-Controlled Key
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await Order.findById(req.params.id);
if (!order) return res.status(404).json({ error: 'Not found'});
res.json(order);
});
認証チェック:はい。所有権チェック:いいえ。
ユーザー42は /api/orders/99 にアクセスすれば、ユーザー99の注文を丸ごと読み取れます。唯一の防御は「正しいIDを知っていること」――そして注文IDは通常、連番の整数か、推測可能なUUIDです。
AIが雑に作っているわけではありません。提示されたタスクを完了しているのです:IDで注文を取得し、認証を要求する。所有権チェックは、プロンプトに入らなかった暗黙のドメイン知識だっただけです。
Why This Keeps Happening
LLMはGitHubとStack Overflowで学習しています。そこにある例は、認証のパターン――ミドルウェア、JWTの検証、セッションチェック――を示しています。所有権チェックはチュートリアルでは比較的レアです。アプリ固有だからです:コードはあなたのデータモデルと、ビジネスルール(「ユーザーは自分の注文にしかアクセスできない」)を知る必要があります。
AIは、あなたのデータモデルを教えてもらわない限り知りません。ほとんどのプロンプトには「そして要求しているユーザーがこのリソースを所有していることを確認してください」が含まれていません。だからAIはそれを飛ばします――怠慢のせいではなく、その制約がプロンプトに書かれていなかったからです。
結果:正しく認証されているのに、完全に未認可。
The Fix
DBクエリに追加の1条件を入れます:
app.get('/api/orders/:id', authenticate, async (req, res) => {
// Query with userId -- DB-level ownership check
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id
});
if (!order) return res.status(404).json({ error: 'Not found'});
res.json(order);
});
2つの理由で、別のifチェックを追加するよりも良いです。1つ目は情報漏えいがないこと――403の代わりに404を返すことで、呼び出し元は「ID 99が存在するのか」「それが他人のものか」を確認できません。2つ目は所有権がクエリのレベルで強制されるため、取得後にチェックし忘れる余地がないことです。
Audit Your Existing Routes
簡単なgrepで候補が見つかります:
grep -rn "req.params" src/ | grep "findById\|findOne"
同じクエリに userId または req.user が含まれていないヒットは、IDORである可能性が高いです。それぞれを手作業で確認してください。
Expressに特化するなら:req.params.id、req.params.postId、req.params.orderId、そしてそれに類するものを使っている全てのルートを確認します。クエリがIDパラメータだけを使っていてユーザーIDを使っていない場合、そのエンドポイントはおそらくこのバグがあります。ネストされたリソースも確認してください――/api/posts/:postId/comments/:commentIdは、検証すべき所有権が2階層ある状態です。
私はこのために SafeWeave を運用しています。CursorやClaude CodeにMCPサーバーとして組み込み、先に進む前にこれらのパターンを検出します。とはいえ、semgrepとgitleaksによる基本的なpre-commitフックでも、この投稿にあるものの大半は拾えます。重要なのは、使うツールにかかわらず、早い段階で検出することです。




