Webアプリの主要セキュリティリスク一覧で触れた「認証・認可の不備」を、具体例とあわせて掘り下げます。
認証と認可は、別のもの
まず言葉の整理から。この2つは似ているのに、指しているものは別です。
| 言葉 | 問うていること |
|---|---|
| 認証(authentication) | あなたは誰ですか?(ログインできるか) |
| 認可(authorization) | あなたは、これをしてよい人ですか?(権限があるか) |
ログイン機能を作ると、多くの場合「認証」はできています。パスワードが合っていればログインできる、という部分です。問題は、そのあとです。 ログインした人が「これを見てよいか」「これを変更してよいか」までを、毎回確認できているとは限りません。
典型的な事故:IDOR
代表的な穴がIDOR(Insecure Direct Object Reference、安全でない直接オブジェクト参照)です。名前は難しく見えますが、起きていることは単純です。
自分の注文履歴を見るページが、こういうURLだったとします。
https://example.com/orders/3
3という番号は、自分の3番目の注文だとします。ここで、番号を4に変えてアクセスしたら、他人の注文が見えてしまった——これがIDORです。
なぜ起きるかというと、サーバー側の処理が「ログインしているか」だけを確認して、「この注文は、リクエストしてきた本人のものか」までは確認していないからです。ログインチェック(認証)は通っているので、エラーにならずにそのままデータが返ってきます。
ブラウザの表示を隠すだけでは対策にならない
「ボタンやリンクを、権限のない人には表示しない」という対策だけでは不十分です。画面に表示されていなくても、そのデータを返すAPI自体には、URLさえ分かれば誰でも直接アクセスできるからです。ブラウザの開発者ツールでAPIの呼び出し先を見れば、隠した画面の裏側にあるURLは簡単に分かります。
見た目を隠すのは利便性のための工夫であって、セキュリティの対策はサーバー側で行う必要があります。
対策の具体策
- すべてのAPI・サーバー側の処理で、データを返す前に「これは誰のデータで、リクエストしてきた人はそれを見る権限があるか」を確認する。 「ログインしているか」のチェックとは別に、「このデータの持ち主は本人か」のチェックを、データを返す直前に必ず入れる
- 連番のIDに頼りすぎない。
1, 2, 3...のような連番は、他人の番号を推測しやすくします。UUID(推測しにくいランダムな識別子)を使うと当てずっぽうでの到達は難しくなりますが、これは補助的な対策です。 IDが推測しにくくても、権限チェック自体を省略していい理由にはなりません - データベース側でも権限を強制する仕組みがあるなら活用する。 SupabaseやFirebaseのようなサービスには、RLS(Row Level Security、行単位のセキュリティ)と呼ばれる、「このデータは、このユーザーしか読み書きできない」という規則をデータベース自身に持たせる仕組みがあります。アプリ側のコードでチェックを書き忘れても、データベース側で防げるという二重の備えになります
- 管理者用のページ・APIは、権限チェックをひときわ厳重にする。 一般ユーザー向けの機能より、影響範囲が大きいためです
エージェントへの頼み方
「ユーザー個別のデータを返しているAPI・ページを一覧にして、それぞれで『リクエストしてきた人がそのデータの持ち主かどうか』のチェックが入っているか確認して」のように頼むと、抜けている箇所を洗い出しやすくなります。IDORはコードを読んだだけでは気づきにくいので、URLの番号を変えて実際にアクセスしてみる、という手動での確認も有効です。
まとめ
- 認証(誰か)と認可(してよいか)は別物。 ログイン機能があっても、認可のチェックが抜けていることがある
- 典型的な事故がIDOR——URLの番号を変えるだけで、他人のデータにアクセスできてしまう穴
- 画面から隠すだけでは対策にならない。 APIは直接叩かれることを前提に、サーバー側で権限を確認する
- 対策の基本は、データを返す直前に「持ち主は本人か」を毎回確認すること。RLSのような仕組みがあれば、データベース側の備えも重ねる
「ログインできたから安全」ではなく、「ログインした人が、そのデータを見てよい人か」まで確認して、はじめて安全と言えます。