仕組みから学ぶ Web
応用読了目安 14#認可#IDOR#アクセス制御#マルチテナント

認可 — ログインしている人が、そのデータを触ってよいとは限らない

URL の注文番号を1つ変えるだけで、他人の注文と住所が見えた。ログインを確かめることと、そのデータを触ってよいかを確かめることは、別の仕事。

この記事の進み方

What — 認証と認可は、別の問いに答える

ログインまわりの処理は、2つの別々の問いに答えています。

  • 認証 — このリクエストを送ってきたのは誰か
  • 認可 — その人は、このデータに、この操作をしてよいか

認証が済んでいても、認可は1つも済んでいません。冒頭の API は「会員の 30821 さんです」までは確かめていましたが、「30821 さんは注文 10492 を見てよいか」は問うていませんでした。

Figureログインは確かめたが、持ち主は確かめていない
利用者会員 30821APIDBGET /api/orders/10492セッションを確認 → 会員 30821SELECT * FROM orders WHERE id = 10492会員 18877 の注文200 + 他人の氏名・住所
1/5
GET /api/orders/10492自分の注文番号は 10493。1つ減らしただけ。ブラウザの開発者ツールや curl で、画面を通さずに送れる。

API は正しくログインを確認している。足りないのは、取り出した注文が、ログインしている人のものかどうかの確認。どこにもエラーは出ない。正常に 200 が返る。

認証
リクエストの送り主が誰かを確かめること。ログイン、セッション、トークンの検証。
認可
その送り主が、そのデータにその操作をしてよいかを判断すること。
IDOR
Insecure Direct Object Reference。ID を差し替えるだけで他人のデータに届いてしまう欠陥。
テナント
1つのシステムを共有する利用単位。会社ごと、店舗ごとなど。データは互いに見えてはいけない。

認可は3つの粒度で要る

「この人がしてよいか」は、何に対して問うかで3段階に分かれます。冒頭の事故は真ん中です。

Compare認可の3つの粒度

この人は、この操作そのものをしてよいか

DELETE /admin/users/42   ← 一般会員が呼んだ
判断に要るもの
利用者の役割だけ
書ける場所
ルーティング、ミドルウェア
よくある抜け
管理画面の API に役割の確認が無い
見つけやすさ
比較的見つけやすい
  • 画面に管理メニューを出さないことは、API を守ることにならない
  • ルートごとに必要な役割を宣言し、宣言の無いルートは拒否する形にする
  • 「管理者用だから URL を知られなければ大丈夫」は成り立たない

上の粒度ほど、ルーティングやミドルウェアで一律に書ける。下の粒度ほど、データの中身を見ないと判断できない。事故が多いのは、一律に書けない側。

確認 — ここまで読めたか

API に「ログインしていなければ 401 を返す」ミドルウェアを入れました。これで防げるのはどれですか?

まず選ぶ(解答例は a〜d の記号で説明します)

Why — なぜ抜けるのか

冒頭の API を書いた人が、認可を知らなかったわけではありません。抜ける理由は、認可が「書き足す」形になっていることにあります。

「取り出してから確かめる」は、書き忘れる

よくある書き方はこうです。

const order = await db.orders.find(id);
if (order.userId !== session.userId) return notFound();   // ← この1行
return order;

この1行を書き忘れても、何も壊れません。 自分の注文で試せば正しく表示され、テストも通り、画面も動きます。書き忘れが見つかるのは、誰かが他人の ID を送ったときだけです。

認可の欠陥は、正常系のテストでは原理的に見つからない種類の欠陥です。

持ち主を条件に入れて取り出す

取り出すときの条件に、ログインしている人を含めます。

SELECT * FROM orders WHERE id = :id AND user_id = :currentUserId

他人の注文番号を送っても、そもそも1件も取り出されません。確かめる1行が無いので、書き忘れようがありません。

さらに一歩進めて、データを取り出す関数が、ログインしている人を引数に取らないと呼べない形にしておくと、条件を付け忘れる経路そのものが無くなります。

画面に出していないことは、守りにならない

冒頭では、画面に他人の注文へのリンクを出していませんでした。それは普通に使う人が迷い込まないようにしているだけです。

API は、画面を通さずに呼べます。ブラウザの開発者ツール、curl、スクリプト。画面が送らないリクエストを、誰も送らないとは限りません。 画面での制御は使いやすさのためで、認可は API の側にしか置けません。

他人のデータには 404 を返す

他人の注文を要求されたとき、403(権限が無い)を返すと、**「その番号の注文は存在する」**と教えてしまいます。存在しないときと同じ 404 を返せば、存在の有無も漏れません。

持ち主を条件に入れて取り出す形なら、他人の注文は「見つからない」ので、自然に 404 になります

確認 — ここまで読めたか

注文番号を連番から UUID に変えました。認可の確認は入れていません。この変更で何が変わりますか?

まず選ぶ(解答例は a〜d の記号で説明します)

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

注文 API の認可を、どこにどう入れますか?

与えられた条件
  • 注文 API は、取得・一覧・キャンセル・配送先の変更の4つ
  • 会員は自分の注文だけを扱える。サポート担当者は全会員の注文を閲覧でき、キャンセルもできる
  • API はルーティング → ハンドラ → サービス → リポジトリ(DB アクセス)の層に分かれている
  • 開発者は6人。新しい API は月に数本増える
  • 今回の事故で、同じ形の欠陥がほかの API にもあると考えている
この軸で考える
  • · 持ち主を確かめるのに、どの層なら必要な情報が揃っているか
  • · 新しい API を足した人が、確認を書き忘れたら何が起きるか
  • · サポート担当者のような例外を、どう表すか
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

「ログインしないと使えないので、セキュリティは大丈夫です」と言う開発者に、何が足りないかを説明してください

与えられた条件
  • 相手はログインの仕組み(セッションや JWT)は理解している
  • 作っているのは、会員が自分の予約を確認・変更する API
  • テストはログインした自分のアカウントで行っている
  • 指摘を受け入れやすい言い方をしたい

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。