コース目次 / 第2章
IDOR — 持ち主を確かめずに返す穴
URLのIDを変えるだけで他人のメモが読める認可の穴を突き、サーバ側で持ち主を照合する修正を入れて、再検証します。
この章は攻撃をします(自作環境専用)。相手は自分のパソコンの shanai-memo だけです(法と倫理)。
IDOR(Insecure Direct Object Reference / 安全でない直接オブジェクト参照)は、IDでデータを引くとき、それが要求者のものかを確かめない穴です。ツールで自動検出しにくく、実務でも高頻度・高価値。URLの数字を1つ変えるだけで、他人のデータが見えてしまいます。
法と倫理ゲート
手を動かす前に
このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。
- (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
- (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
- (c) CTF 競技の中(規約の範囲で)
実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。
同意すると、この先の内容が表示されます。
穴のある場所を読む
shanai-memo では、メモは /api/notes/:id で取れます。vulnerable/server.mjs の該当部分です。
const apiMatch = /^\/api\/notes\/(\d+)$/.exec(pathname);
if (apiMatch) {
const id = Number(apiMatch[1]);
const note = notes.find((n) => n.id === id);
...
res.end(JSON.stringify(note)); // ← 持ち主チェックが無い(穴)
}id で引いたメモを、誰が要求しているかを一切見ずにそのまま返しています。メモ #1 は alice、#2 は bob、#3 は carol のもの。alice でログインしていても、/api/notes/2 を叩けば bob のメモが返ります。
攻める
ブラウザで「alice でログイン」した状態を思い出してください。alice が持っているのはメモ #1 だけです。ところが——
node exploit/idor.mjs[IDOR] 成立: alice のセッションで bob のメモ #2 を読めました(持ち主チェックが無い)手で確かめると、alice のセッションクッキーを付けて #2 を要求するだけで、中身が返ります。
curl -s -H "cookie: session=alice" "http://127.0.0.1:4599/api/notes/2"{"id":2,"owner":"bob","title":"bob の個人メモ(架空)","body":"架空の私的メモ。持ち主は bob。alice には見えてはいけない内容。"}"owner":"bob" のメモが、alice に返ってしまいました。ログインはしているのに、そのデータを見る権利があるかは誰も確かめていません。認証(誰であるか)と認可(何をしてよいか)は別物で、ここで欠けているのは認可です。
直す
修正の芯は、ログイン利用者と、データの持ち主が一致するときだけ返すこと。/api/notes を次のように変えます(currentUser はすでにこのファイルにある関数です)。
const apiMatch = /^\/api\/notes\/(\d+)$/.exec(pathname);
if (apiMatch) {
const me = currentUser(req);
const id = Number(apiMatch[1]);
const note = notes.find((n) => n.id === id);
if (!note) {
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'not found' }));
return;
}
if (!me || note.owner !== me) {
res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: 'forbidden' }));
return;
}
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(note));
return;
}他人のメモには 404 ではなく 403 を返しています。もし「無いものは404・あるものは403」と返し分けると、応答の違いからメモの存在の有無が漏れます(オラクルになる)。存在を伏せたいなら、一律 403 か一律 404 に揃えます。ここでは「権限が無い」を素直に示す 403 にしました。
同じ考え方は、ブラウザ用の /notes/:id にも要ります(こちらも持ち主照合を足します)。認可は、データを返すすべての入口に必要です。
再検証する
サーバを再起動して、もう一度攻めます。
node exploit/idor.mjs[IDOR] 防御されています: HTTP 403(他人のメモは返らない)curl でも、alice からの #2 は 403 になりました。
HTTP/1.1 403 Forbidden
{"error":"forbidden"}自分のメモ(alice なら #1)はこれまで通り読めて、他人のメモだけが返らない——認可が効いた状態です。
持ち帰る一言
認証と認可は別。IDで引いたら、要求者のものかを毎回サーバで確かめる。 クライアントに「他人のボタンを隠す」だけでは守りになりません。URLは誰でも書き換えられるからです。守りは必ずサーバ側に置きます。
次章は、外から来た値をそのまま行き先にしてしまう穴——オープンリダイレクトです。
こうなっていればOK
卒業まであと2章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。