なぜ、あなたの「IDOR」は防げないのか?――脆弱性の盲点と、泥臭いサーバーサイド実装の鉄則
現場でコードをレビューしていると、いまだに「URLのIDを少し変えるだけで他人のデータが見えてしまう」という、初歩的かつ致命的なミスに出くわす。これが IDOR(Insecure Direct Object Reference:安全でない直接オブジェクト参照) だ。
クロスサイトスクリプティング(XSS)のような派手な見た目の脆弱性とは違い、IDORは静かに、しかし確実に機密データを流出させる。攻撃者はツールを回してIDをインクリメントするだけで、あなたの会社の顧客データベースを全件ダンプできる。
今日は、教科書的な「認可をしましょう」という抽象論ではなく、なぜこれが起きるのか、そしてどうすれば「絶対に漏れない」状態を作れるのかを、現場の視点で語る。
—
1. なぜIDORは「攻撃」として認識されにくいのか?
IDORが厄介なのは、「認証」と「認可」を混同しているエンジニアが多いことに起因する。
- 認証(Authentication): 「あなたは誰か?」を確認すること。ログイン機能はここだ。
- 認可(Authorization): 「あなたは、そのデータにアクセスする権限を持っているか?」を確認すること。
IDORは、ログインさえしていればパスを通過できる。攻撃者は有効なセッションIDを持ってさえいれば、あとは GET /api/v1/user/1001 の 1001 を 1002 に書き換えるだけだ。WAFをどれだけチューニングしても、正常なアクセスに見えるため、これだけでは防げない。
攻撃者が見ている「甘い実装」
攻撃者は、連番のIDだけでなく、推測可能なUUIDや、DBの内部IDをそのままエンドポイントに露出しているアプリを好む。?user_id=55 といったパラメータは、彼らにとって「どうぞ中を見てください」という招待状に等しい。
—
2. 認可を「フロントエンド」に任せてはいけない
よくある失敗パターンは、フロントエンド側で「ログインユーザーのIDとリソースのIDが一致しなければ非表示にする」という実装だ。これはセキュリティではない。ただのUIの制御だ。
鉄則:認可は必ず「サーバーサイド」の「DBクエリ発行直前」に行え。
リソースを取得する際、単にIDで検索するのではなく、「セッションに含まれるユーザーID」をクエリの条件に含める。これが唯一の正解だ。
—
3. 実践コード:IDORを完全に排除する実装例
PHP(PDO)とPython(FastAPI/SQLAlchemy)での実装例を示す。これらを参考に、既存コードの認可ロジックを見直してほしい。
PHP/PDOでの実装(ダメな例 vs 良い例)
// ❌ ダメな例:IDだけで取得。これだと他人の情報も取れてしまう
$stmt = $pdo->prepare(“SELECT FROM invoices WHERE id = ?”);
$stmt->execute([$_GET[‘id’]]);
// ✅ 良い例:ログインユーザーのIDをWHERE句に必ず含める
// セッションから取得した user_id を「所有権のバリデーター」として使う
$stmt = $pdo->prepare(“SELECT FROM invoices WHERE id = ? AND user_id = ?”);
$stmt->execute([$_GET[‘id’], $_SESSION[‘user_id’]]);
Python/FastAPIでの実装例
from fastapi import Depends, HTTPException, status
from sqlalchemy.orm import Session
認可を強制する依存関係(Dependency Injection)
def get_owned_invoice(invoice_id: int, db: Session, current_user: User = Depends(get_current_user)):
# クエリに所有者IDを強制的に含めることで、IDORを物理的に防ぐ
invoice = db.query(Invoice).filter(
Invoice.id == invoice_id,
Invoice.user_id == current_user.id # ここが認可の壁
).first()
if not invoice:
# 存在しない場合や他人のIDの場合は404を返す(403にすると存在確認に使われるため)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return invoice
—
4. インフラ・設計レベルでの防御戦略
コードの実装以外にも、以下の防御策を組み合わせることで、万が一の漏洩リスクを最小化できる。
1. 推測困難なID(UUID v4/v7)の使用:
連番ID(1, 2, 3…)は攻撃者のインクリメント攻撃を容易にする。UUIDを使用することで、総当たり攻撃を事実上不可能にする。
2. ハッシュ化されたID(HashID)の利用:
公開用のIDと内部のDB IDを分離する。Hashids ライブラリなどを使い、URL上ではランダムな文字列に見せかけ、サーバー内部で元の数値IDに変換する。
3. 適切なHTTPステータスコード:
認可エラーやリソース未発見時には、攻撃者にヒントを与えないことが重要だ。他人のリソースを叩いた時に 403 Forbidden を返すと「そこにデータがある」と教えてしまうことになる。基本的には 404 Not Found を返すのが定石である。
—
最後に:セキュリティは「性悪説」で設計せよ
IDORを防ぐための実装は、最初は面倒に感じるかもしれない。しかし、インシデントが発生した後の対応コスト(謝罪会見、DBの調査、被害者への補償)に比べれば、開発時のこの手間は「極めて安い保険」だ。
「自分たちのコードは誰も見ていないから大丈夫」というのは、セキュリティにおいては最も危険な妄想だ。「すべてのパラメータは改ざんされるもの」、「すべてのリクエストは攻撃者が行っているもの」 という前提で設計しよう。
君たちが書くコードの一行一行が、ユーザーの信頼を守る最後の砦だ。まずは明日の朝、リポジトリの WHERE 句を全検索するところから始めてみてほしい。そこに、君たちのシステムの「穴」が隠れているはずだ。
コメント