こんにちは!Webアプリケーションの開発やセキュリティに触れ始めたばかりのIT担当者の皆さん、日々の開発や運用お疲れ様です。
「セキュリティ対策」と聞くと、なんだか難解な暗号や、映画に出てくるようなハッカーの攻防をイメージして身構えてしまいますよね。でも、実はもっと身近な、私たちの日常生活にある「鍵のかけ忘れ」のようなうっかりミスが原因で、大きなセキュリティ事故につながることがあるんです。
今回は、Webアプリの代表的な脆弱性の一つである IDOR(Insecure Direct Object Reference:不安全な直接的オブジェクト参照) について、私たちの身近な防犯にたとえながら、その仕組みとしっかりとした対策を一緒に学んでいきましょう!一歩ずつ、優しく解説していくので安心してくださいね。
—
1. 身近な例えで理解する「IDOR(アイドア)」の正体
突然ですが、あなたがとある高級マンションの管理人さんだと想像してみてください。
このマンションの各部屋には、郵便受けやロッカーが設置されています。
セキュリティが完璧な状態とは、「101号室の住人は、101号室の鍵を使って自分のロッカーしか開けられない」状態ですよね。
では、もし以下のような状態になっていたらどうでしょうか?
- ロッカーの番号(ID)が、ただの通し番号になっている(
1,2,3…) - ロッカーの鍵穴が全部同じ構造で、101号室の鍵をちょっと差し替えるだけで、隣の102号室や105号室のロッカーまでパカッと開いてしまう
これが、Webの世界でいう IDOR です。
攻撃者は特別なパスワードを破る必要すらありません。ただブラウザのURLやリクエストに含まれる数字を「ちょっと書き換えるだけ」で、他のユーザーの秘密のデータをごっそり盗み見できてしまうのです。
—
2. 実際のWebアプリで何が起きているのか?(攻撃のメカニズム)
もう少し具体的に、Webアプリの裏側を覗いてみましょう。
例えば、あなたが会員制のマイページにログインし、自分の注文履歴(注文ID: 1005)を見ているとします。このとき、ブラウザのURLや裏側の通信(APIリクエスト)は、次のような形になっていることがよくあります。
https://example.com/api/orders?id=1005
ここで、好奇心旺盛な(あるいは悪意を持った)ユーザーが、ふとこう考えます。
「この id=1005 を、id=1004 や id=1006 に変えたらどうなるんだろう?」
脆弱性のあるアプリの場合、ここでサーバー側が「おいおい、このリクエストを送ってきた人は、本当にこの注文IDを見てもいい権限を持っている人だっけ?」という確認(認可チェック)をサボってしまっています。
その結果、サーバーは「はい、どうぞ!」とばかりに、他人のプライベートな注文情報やクレジットカードの下4桁、お届け先の住所などを平然と返してしまうのです。これがIDORの恐ろしいところです。
—
3. なぜこの脆弱性が生まれてしまうのか?
開発現場でよくある原因は、シンプルに「ログインしているか(認証)」の確認だけで満足して、「このデータを触る権利があるか(認可)」の確認を忘れてしまうことです。
- 認証(Authentication): 「あなたは〇〇さんですね(本人確認)」
- 認可(Authorization): 「〇〇さんは、このデータを操作してもいい人ですか?(権限確認)」
「ログイン画面を通っているんだから大丈夫だろう」という思い込みが、認可チェックの抜け道を作ってしまう原因になります。
—
4. 脆弱なコードと安全なコードを見比べてみよう
それでは、実際のPHPのコードを例に、何がダメでどう直すべきなのかを見ていきましょう。
【危険な実装例】(IDORが存在するコード)
以下のコードでは、送られてきた id をそのままデータベースに問い合せて結果を返しています。これでは、URLの id を書き換えるだけで他人のデータが見放題になってしまいます。
<?php
// 危険な実装例:認可チェックが抜けている
session_start();
$login_user_id = $_SESSION['user_id']; // 現在ログインしているユーザーのID
// リクエストから注文IDを取得
$order_id = $_GET['id'];
// 【問題点】ログイン中のユーザーIDと、取得しようとしている注文の所有者が一致するか確認していない!
$sql = "SELECT * FROM orders WHERE id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$order_id]);
$order_data = $stmt->fetch();
// データをそのままJSONで返却
echo json_encode($order_data);
?>
【安全な実装例】(RBAC/ABACを取り入れた堅牢なコード)
では、どうすれば安全になるでしょうか?
「今ログインしているユーザーが、この注文データの持ち主であるか」をデータベースのクエリやロジックでしっかりと縛りつける(認可チェックを入れる)のが正解です。
<?php
// 安全な実装例:リクエストごとに厳格な認可チェックを行う
session_start();
$login_user_id = $_SESSION['user_id']; // 現在ログインしているユーザーのID
$order_id = $_GET['id'];
// 【対策】WHERE句に `user_id = ?` を必ず含め、他人のデータを絶対に取得できないようにする
$sql = "SELECT * FROM orders WHERE id = ? AND user_id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$order_id, $login_user_id]);
$order_data = $stmt->fetch();
// データが存在しない(または他人のもの)場合はエラーを返す
if (!$order_data) {
http_response_code(403); // アクセス権限がないことを示すHTTPステータス
echo json_encode(["error" => "アクセス権限がありません、またはデータが存在しません。"]);
exit;
}
// 正常なデータを返却
echo json_encode($order_data);
?>
このように、「リクエストされたリソースのID」と「現在のユーザーがアクセス可能な範囲」をサーバー側で突き合わせることが、IDORを防ぐための最大の防御策になります。
—
5. 実務で役立つ!IDORを防ぐための設計・実装チェックリスト
最後に、明日からの開発やインフラ・アプリの設計でそのまま使えるチェックリストをまとめました。ぜひチームメンバーとも共有してみてくださいね。
1. 連番ID(予測可能なID)をそのまま使わない
- データベースのオートインクリメント(
1,2,3…)をそのままURLやAPIの識別子に使うのは避けましょう。どうしても隠したい機密性の高いリソースには、推測困難なUUID(v4)やハッシュ値を利用するのも有効なアプローチです(※ただし、UUIDを使う場合でも認可チェック自体を省いていい理由にはならないので注意してください!)。
2. すべてのエンドポイントで「認可(Authorization)」を忘れない
- 「画面遷移のリンクを知っている人しか辿り着けないから大丈夫(隠匿性によるセキュリティ)」という考え方は捨てましょう。APIやURL直接叩きに対するガードは必須です。
3. ロールベース(RBAC)や属性ベース(ABAC)のアクセス制御をミドルウェアで共通化する
- 個別のプログラムファイルごとに認可チェックを書くのは、書き忘れやバグの元になります。フレームワークのポリシー機能やミドルウェアを使い、リクエストのたびに自動で権限が検証される仕組みを共通基盤として整えましょう。
—
まとめ
IDORは、仕組み自体はとてもシンプルですが、ひとたび放置すると顧客情報の流出など甚大なインシデントにつながる怖い脆弱性です。
ですが、「誰がどのリソースにアクセスしてよいかを、サーバー側で毎回必ずチェックする」という基本さえ押さえておけば、確実に防ぐことができます。
「自分の書いたコード、大丈夫かな?」と不安になったときは、ぜひ今回の記事を思い出して、テスト環境でURLのIDを書き換えてみるテスト(いわゆる自社アプリへのポジティブなセキュリティ確認)を試してみてくださいね。
一歩ずつ、安全で信頼されるWebアプリケーションを一緒に作っていきましょう!
コメント