ログインした利用者が URL の数字を1つ変えたら、別の人の請求書が表示された。これが IDOR(認可漏れ)です。ID を推測されにくい値に変えても、ID はリンクやログから漏れるので、止めるのは「このデータはこの人のものか」という確認だけです。本記事では、IDOR がどこに潜み、どう見つけ、どう直すかを、Flask で書いた小さなアプリで脆弱な状態と修正後を実際に動かして確かめます。

ONE LINE OF SQLIDORの正体

差は所有者で絞るかどうかの1行だけ

IDだけで探す(脆弱)

WHERE id = ?

  • BがAの請求書を要求200 とAの金額が返った
  • テストは落ちるpytest 1 failed

所有者で絞って探す(修正後)

WHERE id = ? AND owner_id = ?

  • BがAの請求書を要求404(存在しないのと同じ応答)
  • テストは通るpytest 1 passed
出典: 本記事の実測(Flask 3.1.3 + SQLite・2026-10-17)

30秒でわかる

  • ・IDOR は OWASP API Security Top 10 2023 の1位(BOLA)。ID が連番でも UUID でも起きる
  • ・潜むのは GET だけではない。更新・削除、親子の URL、ファイル、GraphQL、エクスポート、owner_id の代入
  • ・見つけ方の基本は2つのアカウント。A が作ったデータの ID を、B のログインで要求する
  • ・直し方は所有者で絞った検索。実測では SQL に AND owner_id = ? を足すだけで 200 が 404 に変わった
  • ・同じ確認を pytest に書いておけば、脆弱な版では落ち、修正後は通る

この連載は、実際の事故と開発者の議論から Web アプリの守り方を1回1テーマで整理しています。セキュリティ分野の記事の全体像はサプライチェーン攻撃とは|手口・防御ツール比較・npm 12の新機構まで実践解説から辿れます。

10月13日、NICT は国内で相次ぐ情報漏えいの原因の一つとして「A2B(App to Backend)攻撃」を注意喚起しました。公開アプリを解析して API の接続先や通信の形を割り出し、裏のサーバを直接狙う手口です。アプリ自体に脆弱性が無くても、サーバ側の認証・認可や入力の検証に不備があれば漏えいにつながります。NICT が事業者に重点的な点検を求める脆弱性の筆頭が、この記事で扱う認可不備(IDOR/BOLA)です。

IDOR(認可漏れ)とは:OWASPが1位に置くBOLA

IDを差し替えるだけで他人のデータに届く

OWASP の IDOR 対策チートシートは、IDOR の材料を3つに分けています。請求書や文書のようなオブジェクト、それを指す ID やファイル名などの参照、そしてオブジェクト単位の認可チェックの欠落です。3つ目が欠けているときだけ不具合になります。

API の文脈では同じものを BOLA(Broken Object Level Authorization)と呼びます。OWASP API Security Top 10 2023 は BOLA を1位(API1)に置き、悪用のしやすさ・広がり・見つけやすさをすべて最も高い段階(Easy / Widespread / Easy)と評価しています。理由として挙げているのは、サーバーが利用者の状態を追わず、クライアントが送る ID を頼りにどのデータを扱うか決めがちなことです。Web アプリ全体の OWASP Top 10 でも、IDOR を含む「アクセス制御の不備」は2021年版の A01 で、2025年版でも A01 のままです。

影響範囲:見えるだけで終わらない

OWASP は BOLA の影響を、第三者へのデータの開示、データの消失、改ざんとし、条件によってはアカウントの乗っ取りに至るとしています。ID は連番の整数、UUID、文字列のどれでもあり得て、型にかかわらずリクエストの中で見つかる、とも書いています。推測されにくい ID は列挙を難しくするだけです。

もう1つ見落としやすい点があります。OWASP は、セッションの利用者 ID とパラメータの ID を比べるだけでは一部のケースしか防げないと書いています。比べるべきは「この利用者が、この操作を、このオブジェクトに対して行ってよいか」です。

IDORが潜む場所:GET以外に見落としやすい処理

WHERE IT HIDESIDORが潜む場所

見えているのはGET /items/{id}だけ

IDを受けて1件返すAPI(レビューで真っ先に見る): GET

IDを受け取るのに見落とされがちな処理:

  1. 更新・削除・状態の変更(PUT・DELETE)
  2. 親子のURL・ファイルのダウンロード・エクスポート
  3. GraphQLのID引数・一括処理・管理画面のAPI
  4. リクエストの owner_id をそのまま保存する処理

IDを受け取るすべての処理で、所有者の確認が要る

出典: OWASP API Security Top 10 2023 API1・API3、IDOR Prevention Cheat Sheet

コードレビューで真っ先に見るのは GET /invoices/{id} のような1件取得です。ただ、OWASP のチートシートは、確認すべき操作として読み取り・作成・更新・削除・エクスポート・管理系の操作を挙げています。ID を受け取る処理はすべて対象です。

潜む場所 確かめ方(B のログインで) 直し方
1件取得 GET /invoices/{id} A の ID を要求する 所有者で絞った検索にする
更新・削除 A の ID を更新・削除し、A 側で中身を確認 書き込みにも同じ絞り込みを使う
親子の URL /orgs/{a}/projects/{b} 自分の組織の URL に、他の組織のプロジェクト ID を入れる 子を親からたどって取得し、親の所属も確かめる
ファイルのダウンロード A のファイルの URL を開く 静的ファイルも同じ認可の仕組みに通す
GraphQL の ID 引数・一括処理 ID の配列に A の ID を1つ混ぜる 配列の要素ごとに確かめる
一覧・検索・エクスポート 結果に A の行が混ざるかを見る 検索条件そのものを所有者で絞る
作成時の owner_id リクエストに owner_id を A として送る 所有者はサーバーがログイン情報から決める
管理画面の API 一般ユーザーで呼ぶ 機能単位の認可(OWASP の API5)で止める

GraphQL の例は OWASP 自身が API1 のシナリオで挙げています。文書の ID を配列で受け取る削除の mutation が、権限を確かめずに他人の文書を消せてしまうというものです。owner_id の書き換えは API3(オブジェクトのプロパティ単位の認可)に含まれる一括代入の問題で、受け取った値をそのまま保存すると他人の持ち物を作れてしまいます。非同期のエクスポートのように、ジョブの中でリクエストの利用者が見えなくなる処理も、依頼した人の ID をジョブに渡して同じ絞り込みをかけるのが安全です(ここは筆者の設計判断です)。

管理系の認可漏れが重大事故になった例として、SiYuan 脆弱性 CVE-2026-66012|CVSS10.0のMCP認可漏れで31ツールが未認証到達もあわせて読めます。

IDORの見つけ方①:2つのアカウントで他人のIDを差し込む

TWO ACCOUNT TEST見つけ方

2つのアカウントで、他人のIDを差し込む

  1. AとBを作り、Aでデータを作る

    請求書・文書など。権限の違う管理者も用意する

  2. BのログインでAのIDを要求する

    読む・更新・削除・エクスポートを全部

  3. Bは拒否、Aは成功を確かめる

    本人が読めること(陽性対照)も同時に見る

出典: OWASP IDOR Prevention Cheat Sheet・WSTG-ATHZ-04

OWASP のテストガイド(WSTG-ATHZ-04)とチートシートは、少なくとも2人の利用者を用意し、一方のデータの ID をもう一方で要求するやり方を勧めています。ID を推測する手間が要らず、ID が予測しにくい値でも試せるからです。

本記事では、請求書を1件返す API を Flask と SQLite で書き、SELECT * FROM invoices WHERE id = ? と ID だけで検索する版を用意しました。トークン tokenA が利用者 A、tokenB が利用者 B です。次のコマンドを実際に流しています。

# A のトークンで請求書を作る(返った id を控える)
curl -s -X POST -H 'Authorization: Bearer tokenA' -H 'Content-Type: application/json' -d '{"amount": 12000}' http://127.0.0.1:5000/invoices
# 本人(A)と他人(B)で同じ id を読む。B が 200 なら IDOR
curl -s -o /dev/null -w '%{http_code}\n' -H 'Authorization: Bearer tokenA' http://127.0.0.1:5000/invoices/1
curl -s -o /dev/null -w '%{http_code}\n' -H 'Authorization: Bearer tokenB' http://127.0.0.1:5000/invoices/1

脆弱な版では、A も B も 200 でした。ステータスを出さずに本文を表示させると、B のリクエストに A の請求書("amount":12000,"owner_id":1)がそのまま返りました。修正版を同じポートで起動して同じ3行を流すと、A は 200、B は 404 に変わりました。

大事なのは、A が読めること(陽性対照)を同時に確かめることです。B が 404 でも、A まで 404 なら API が壊れているだけで、認可が効いた証拠になりません。

また、このコマンドが確かめたのは GET /invoices/1 の読み取り1経路だけです。B が 404 になっても、更新・削除・エクスポートや親子の URL に IDOR が無いとは限らないので、同じ「A で作って B で要求する」をそれぞれ流して確かめます。

IDORの見つけ方②:コードレビューで探す書き方

レビューでは「リクエストから来た ID が、所有者の条件なしに検索へ入っている行」を探します。次の書き方が出発点です。

フレームワーク 危ない書き方 所有者で絞った書き方 確認した資料
Rails Project.find(params[:id]) @current_user.projects.find(params[:id]) Rails セキュリティガイド・OWASP
Spring findById(id) findByIdAndOwnerId(id, currentUser().getId()) OWASP チートシート
Django Invoice.objects.get(pk=pk) Invoice.objects.get(pk=pk, owner=request.user) Django ドキュメント
Express 系 findById(req.params.id) 所有者の条件を検索に足す (書き方の例)
生の SQL WHERE id = ? だけ WHERE id = ? AND owner_id = ? 本記事の実測

フレームワークの仕組みには、効く範囲に癖があります。資料で確かめた範囲で挙げます。

・Django の権限の仕組みはオブジェクト単位の権限の土台だけを持ち、本体には実装が無い。既定のままではオブジェクトの権限確認は常に偽を返す
・Django REST framework の汎用ビューは get_object() の中でオブジェクトの権限を確かめるが、一覧の取得と作成では適用しない。自前のビューや get_object の上書きでは check_object_permissions を自分で呼ぶ必要がある
・Laravel はモデルごとの Policy と Gate::authorize を持ち、拒否を 404 で返す denyAsNotFound も用意している
・Spring Security の @PreAuthorize は、@EnableMethodSecurity で有効にしないと働かない(OWASP のチートシートによる)

認可の確認を書いたつもりでも、条件の組み方次第で「確かめられないときは通す」動きになることがあります。実例はnext-auth 脆弱性|認可チェックがfail-openするGHSA2件、v5ベータが対象で扱いました。

IDORの対策:所有者で絞る検索と、落ちるテスト

検索の段階で所有者・テナントを絞る

取ってから比べるより、最初から自分のデータの中だけを探すほうが漏れにくくなります。OWASP のチートシートも、Rails の @current_user.projects.find や Spring の findByIdAndOwnerId のように、利用者がアクセスできるデータの集合から探す方法を先に勧めています。複数の会社が同じ表を使うなら、テナント ID の条件も同じ場所で足します。データを読む関数を find_invoice_for(user, id) のように所有者を必須の引数にしておけば、条件の付け忘れがレビューで目立ちます。

既定で拒否し、判断を1か所に集める

OWASP Top 10 は、公開データ以外は既定で拒否し、アクセス制御は誰でもどの記録でも読み書きできる形ではなく、記録の所有権を強制する形にするよう求めています。Authorization チートシートは、認可の確認を処理ごとに散らさず、アプリ全体で設定できる仕組みで行い、拒否したときの処理も1か所にまとめるよう勧めています。Policy のようなフレームワークの仕組みがあるなら、そこに集めます。

404と403、拒否の記録

他人のデータに 403 を返すと「その ID のデータは存在する」と教えてしまいます。存在を知られたくないなら、OWASP のチートシートの例のように、所有者で絞った検索で「無い」と「見せられない」を同じ 404 にそろえます。一方で、拒否した事実は記録します。OWASP Top 10 は、アクセス制御の失敗を記録し、繰り返されたら管理者に知らせ、API の呼び出し回数を制限するよう挙げています。本記事のアプリでも、B の要求は invoice not found or denied: user=2 invoice=1 としてログに残りました。

落ちるテストをCIに置く

OWASP は API1 の対策として、認可の仕組みを確かめるテストを書き、テストが落ちる変更はデプロイしないよう求めています。本記事で書いたテストは次のとおりです。

from app import create_app


def test_other_users_invoice_is_not_readable():
    client = create_app().test_client()
    created = client.post("/invoices", json={"amount": 12000},
                          headers={"Authorization": "Bearer tokenA"})
    invoice_id = created.get_json()["id"]

    as_owner = client.get(f"/invoices/{invoice_id}", headers={"Authorization": "Bearer tokenA"})
    as_other = client.get(f"/invoices/{invoice_id}", headers={"Authorization": "Bearer tokenB"})

    assert as_owner.status_code == 200  # 陽性対照: 本人は読める
    assert as_other.status_code == 404  # 他人は読めない

python -m pytest -q test_idor.py を流すと、ID だけで検索する版では assert 200 == 404 で 1 failed、所有者で絞った版では 1 passed でした。新しいエンドポイントを足すたびに、この「A で作って B で読む」を1本ずつ足していけば、潜む場所の表をテストで埋めていけます。

この記事で確かめたことと、確かめていないこと

項目 状態
ID だけで検索する API で、B が A の請求書を読めること 実測(curl で 200 と本文を確認)
所有者で絞ると B が 404 になり、A は 200 のままなこと 実測
pytest が脆弱な版で落ち、修正後に通ること 実測
拒否がログに残ること 実測(本記事のアプリの実装として)
Rails・Django・DRF・Laravel・Spring の仕組みの挙動 公式資料・OWASP で確認。動かしてはいない(未検証)
親子の URL・GraphQL・ファイル・エクスポートでの再現 未検証(表は OWASP の記述と筆者の整理)

本記事の実測環境は Ubuntu 24.04.5 LTS(x86_64)、Python 3.13.16、Flask 3.1.3、Werkzeug 3.1.9、pytest 9.1.1、curl 8.5.0 で、2026年10月17日(JST)に手元で起動したアプリだけを相手にしています。他人のサービスに同じ手順を試すのは、許可を得た検証の範囲に限ってください。

参照ソース

以下は 2026年10月17日に、OWASP のリポジトリは raw.githubusercontent.com から、フレームワークの文書は各リポジトリの原稿から本文を取得して確かめました。CWE の各ページは未参照で、番号と名称は OWASP の対応表(API1・Top 10:2025 A01)の記載に拠っています。

・NICT「相次ぐ情報漏えい、その原因の一つは『アプリ解析からのサーバ攻撃』」(2026-10-13)
・OWASP API Security Top 10 2023 — API1:2023 Broken Object Level Authorization
・OWASP Top 10:2025 — A01:2025 Broken Access Control
・OWASP Top 10:2021 — A01:2021 Broken Access Control
・OWASP Cheat Sheet Series — Insecure Direct Object Reference Prevention
・OWASP Cheat Sheet Series — Authorization
・OWASP WSTG — WSTG-ATHZ-04 Insecure Direct Object References
・CWE-639: Authorization Bypass Through User-Controlled Key(CWE-862 Missing Authorization、CWE-863 Incorrect Authorization も A01 の対応表に含まれる)
・Django — Handling object permissions
・Django REST framework — Permissions
・Laravel 12.x — Authorization
・Ruby on Rails Guides — Securing Rails Applications(Privilege Escalation)