認可 — ログインしている人が、そのデータを触ってよいとは限らない
URL の注文番号を1つ変えるだけで、他人の注文と住所が見えた。ログインを確かめることと、そのデータを触ってよいかを確かめることは、別の仕事。
この記事の進み方
What — 認証と認可は、別の問いに答える
ログインまわりの処理は、2つの別々の問いに答えています。
- 認証 — このリクエストを送ってきたのは誰か
- 認可 — その人は、このデータに、この操作をしてよいか
認証が済んでいても、認可は1つも済んでいません。冒頭の API は「会員の 30821 さんです」までは確かめていましたが、「30821 さんは注文 10492 を見てよいか」は問うていませんでした。
API は正しくログインを確認している。足りないのは、取り出した注文が、ログインしている人のものかどうかの確認。どこにもエラーは出ない。正常に 200 が返る。
- 認証
- リクエストの送り主が誰かを確かめること。ログイン、セッション、トークンの検証。
- 認可
- その送り主が、そのデータにその操作をしてよいかを判断すること。
- IDOR
- Insecure Direct Object Reference。ID を差し替えるだけで他人のデータに届いてしまう欠陥。
- テナント
- 1つのシステムを共有する利用単位。会社ごと、店舗ごとなど。データは互いに見えてはいけない。
認可は3つの粒度で要る
「この人がしてよいか」は、何に対して問うかで3段階に分かれます。冒頭の事故は真ん中です。
この人は、この操作そのものをしてよいか
DELETE /admin/users/42 ← 一般会員が呼んだ- 判断に要るもの
- 利用者の役割だけ
- 書ける場所
- ルーティング、ミドルウェア
- よくある抜け
- 管理画面の API に役割の確認が無い
- 見つけやすさ
- 比較的見つけやすい
- –画面に管理メニューを出さないことは、API を守ることにならない
- –ルートごとに必要な役割を宣言し、宣言の無いルートは拒否する形にする
- –「管理者用だから URL を知られなければ大丈夫」は成り立たない
上の粒度ほど、ルーティングやミドルウェアで一律に書ける。下の粒度ほど、データの中身を見ないと判断できない。事故が多いのは、一律に書けない側。
API に「ログインしていなければ 401 を返す」ミドルウェアを入れました。これで防げるのはどれですか?
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 に変えました。認可の確認は入れていません。この変更で何が変わりますか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
注文 API の認可を、どこにどう入れますか?
- 注文 API は、取得・一覧・キャンセル・配送先の変更の4つ
- 会員は自分の注文だけを扱える。サポート担当者は全会員の注文を閲覧でき、キャンセルもできる
- API はルーティング → ハンドラ → サービス → リポジトリ(DB アクセス)の層に分かれている
- 開発者は6人。新しい API は月に数本増える
- 今回の事故で、同じ形の欠陥がほかの API にもあると考えている
- · 持ち主を確かめるのに、どの層なら必要な情報が揃っているか
- · 新しい API を足した人が、確認を書き忘れたら何が起きるか
- · サポート担当者のような例外を、どう表すか
「ログインしないと使えないので、セキュリティは大丈夫です」と言う開発者に、何が足りないかを説明してください
- 相手はログインの仕組み(セッションや JWT)は理解している
- 作っているのは、会員が自分の予約を確認・変更する API
- テストはログインした自分のアカウントで行っている
- 指摘を受け入れやすい言い方をしたい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。