【実務・中級編】 認可制御の欠如(IDOR)による他者データへの不正アクセス – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

IDORの悪夢:なぜ「URLの数字を変えるだけ」でシステムは崩壊するのか

現場で数多のペネトレーションテストを行ってきて、最も「骨が折れる」かつ「呆気なく侵入できる」のがこのIDOR(Insecure Direct Object Reference:不適切な直接オブジェクト参照)だ。

多くの開発者が、「ログインしているから大丈夫だろう」という甘い考えで、URLパラメータのIDを信用しきっている。例えば、ユーザーのマイページを表示するURLが /api/orders?id=1001 だったとする。攻撃者はここで 1001 を 1002 に書き換える。もしサーバーが「今ログインしているユーザー」と「要求されたID」の紐付けを検証していなければ、他人の注文履歴、住所、クレジットカードの下4桁まで、すべて丸見えだ。

これは設定ミスではない。「認可(Authorization)」の実装漏れという、設計上の致命傷だ。

1. 攻撃者が狙う「IDの推測可能性」と「脆弱性の本質」

攻撃者が狙うのは、単なる連番IDだけではない。最近はUUIDを使っていれば安全だと勘違いしているケースも多いが、IDの形式に関わらず「リソースへのアクセス権があるか」をサーバー側でチェックしていないなら、それは等しく脆弱だ。

攻撃者はまず、以下のステップで脆弱性を突く。

1. ベースラインの取得: 自分のアカウントでログインし、正常なレスポンスを確認する。
2. IDの列挙: パラメータのIDを一つずつインクリメント(またはUUIDを総当たり)し、レスポンスコードが 200 OK になるものを探す。
3. データ抽出: スクリプトを走らせ、数万件の個人情報を数分で抜き取る。

これを防ぐ唯一の道は、「クライアントから送られてきたIDは、あくまで『何を見たいか』のヒントに過ぎず、実際に表示して良いかはサーバー側がセッション情報と照らし合わせて決める」という鉄則を守ることだ。

—

2. 【改善前】IDORが起きる典型的な「NGコード」

以下のコードは、多くの現場で見かける「動くが脆い」PHPの例だ。

// 非常に危険な実装例
$order_id = $_GET['id'];
// クエリのIDが他人のものかを確認せず、そのままDBへ問い合わせている
$sql = "SELECT * FROM orders WHERE id = " . $order_id;
$result = $db->query($sql);
echo json_encode($result->fetch());

このコードの何が悪いか? $_GET['id'] を一切信用し、認証後の「所有者チェック」を無視している点だ。

—

3. 【改善後】堅牢なセキュア実装サンプル

では、どう修正すべきか。正解は、「SQLの条件句に必ず所有者ID(user_id)を含めること」だ。

PHPでのセキュアな実装例

// セッションから現在ログイン中のユーザーIDを取得
$current_user_id = $_SESSION['user_id'];
$requested_order_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

if (!$requested_order_id) {
    die("不正なリクエストです");
}

// 重要なのは「WHERE id = ? AND user_id = ?」とすることで、
// 自分以外のリソースには絶対にアクセスさせないこと
$stmt = $pdo->prepare("SELECT * FROM orders WHERE id = ? AND user_id = ?");
$stmt->execute([$requested_order_id, $current_user_id]);
$order = $stmt->fetch();

if (!$order) {
    // 存在しない、あるいは他人のリソースへのアクセスなので404を返す
    // 攻撃者にヒントを与えないため、403ではなく404を返すのがコツだ
    http_response_code(404);
    exit("見つかりません");
}

echo json_encode($order);

4. 開発現場で今すぐやるべき「防御の層」

コード修正が基本だが、防御は多層的であるべきだ。以下のチェックリストをチームのデプロイフローに組み込んでほしい。

  • SQLの条件に必ず所有者を含める: 上記のコードのように、WHERE 句には必ずログインユーザーの権限を縛る条件を入れる。
  • IDの難読化(推奨): 外部公開するIDにはDBの連番(Auto Increment)をそのまま使わず、ハッシュ化やUUID(v4など)を使用して推測困難にする。ただし、これは「認可」の代わりにはならない。あくまで「推測によるスキャン」を遅らせるための防波堤だ。
  • WAFによる保護: AWS WAFやCloudflareなどのWAFで、「特定のURLパターンへの短時間での大量アクセス」をレートリミット(Rate Limiting)で遮断する。IDORを試行するスクリプトを即座に検知・ブロックできる。

Nginxでのレートリミット設定例 (参考)

# APIエンドポイントへのアクセスを制限する
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;

server {
    location /api/orders {
        limit_req zone=api_limit burst=10; # 1秒間に5リクエストまで。超過は429エラー
        proxy_pass http://backend;
    }
}

最後に

IDORは、どんなに強固なファイアウォールを築いても、アプリ層の甘いロジック一つで突破される。

「自分以外のデータを表示しようとしたら、システムは何を返すべきか?」
この問いを、すべてのエンドポイント開発時に自問自答してほしい。それができれば、君のコードからIDORは消滅する。コードを書き終えたら、一度「自分のIDではない値を入れてアクセスしたらどうなるか?」を自分自身でテストする。この一手間が、数億円規模の漏洩事故を防ぐのだ。

エンジニアとしてのプライドを、こうした泥臭い「認可」の実装にこそ注ぎ込んでくれ。

コメント

タイトルとURLをコピーしました