Broken Access Control を技術者として正しく理解する
〜「認証しているのに事故る」最も多い脆弱性〜
Broken Access Control(アクセス制御の不備)は、OWASP Top 10 で常に上位に登場する脆弱性です。
特定の言語やフレームワーク固有の問題ではなく、設計と思考の問題として発生します。
本記事は 初心者〜中級者の Web エンジニア を対象に、
- Broken Access Control とは何か
- なぜ頻発するのか
- よくあるアンチパターン
- なぜアンチパターンに陥るのか
- 特に多い「画面操作前提」という思考
を整理します。
※ ソースコード例は理解しやすさのため Laravel(PHP)で記述しますが、
考え方自体はどの環境でも同じです。
Broken Access Control とは?
Broken Access Control とは、
ログインはしているが、本来許可されていない操作やデータに
アクセスできてしまう状態
を指します。
重要なのは「認証」と「認可」の違いです。
- 認証(Authentication):あなたは誰か?本人確認(ログイン)
- 認可(Authorization):あなたはそのリソース/操作に対する権限を持っているか?
例えば、
- ログインできている → 認証OK
- でも他人の注文を見られてしまう → 認可NG
Broken Access Control は、認可の失敗です。
ログインできているかどうかは、本質ではありません。
なぜ Broken Access Control は多発するのか?
理由はいくつもありますが、
実務で一番多い原因はこれです。
「ユーザーは画面の通りにしか操作できない」
という思い込みです。
アンチパターン①
「ログイン必須にしているから大丈夫」
Route::get('/orders/{id}', function ($id) {
return Order::findOrFail($id);
})->middleware('auth');
何が問題か?
- ログインしていれば、誰の注文でも閲覧できる
- ID を変えるだけで他人のデータにアクセスできる
これを IDOR(Insecure Direct Object Reference:安全でない直接オブジェクト参照) と呼びます。
URLのIDを変えるだけで、本来アクセスできないデータにアクセスできてしまう脆弱性です。
なぜこのアンチパターンに陥るのか?
- 「ログイン必須 = 安全」という思い込み
- 認証と認可を無意識に混同している
- 正常系テストでは必ず成功するため、危険に気づけない
アンチパターン②
「画面に出していないから使えない」
@if($user->is_admin)
<a href="/admin/users">管理画面</a>
@endif
何が問題か?
- URL を直接叩けば誰でもアクセスできる
- 画面制御は UX の話であり、セキュリティではない
なぜこのアンチパターンに陥るのか?
- 「画面に出ていない = 操作できない」という錯覚
- フロントエンドで完結した気になる
- サーバー側の責務を意識していない
【重要】アンチパターンに陥る一番多い理由
画面操作を前提にしてしまっている
Broken Access Control の原因は色々ありますが、
現場で圧倒的に多いのはこれです。
画面上ではこうなっているから大丈夫、という思考。
例えば、
- 注文一覧には自分の注文しか表示されない
- 注文詳細は一覧からクリックしたものしか見られない
- だから他人の注文は見られないはず
※AIによって生成したイラストです
これは UI の制約 を
システムの制約だと勘違い しています。
なぜこの思考が生まれるのか?
① 画面は制限されているように見える
- 表示される情報が限定されている
- 操作できる導線が制御されている
そのため「この操作はできない」と錯覚します。
しかしそれは UI の話であって、サーバーの話ではありません。
② テストも画面操作しかしていない
- テストケース = 画面操作
- URL を直接叩くテストをしていない
結果として、
画面通りに使えば問題ない
という誤った成功体験だけが積み重なります。
③ 攻撃者は画面を見ていない
攻撃者は画面を見ません。
/orders/101
/orders/102
/orders/103
ID を変えて直接叩くだけです。
サーバー側で
- このユーザーは、このデータを見てよいか
を判定していなければ、即アウトです。
正しい基本方針(フレームワーク非依存)
Broken Access Control を防ぐために重要なのは、
- 画面や遷移を一切信用しない
- すべてのリクエストを単体で評価する
- 毎回「このユーザーはこれをしてよいか?」を判断する
という考え方です。
実装イメージ(考え方の一例)
パターン1: 明示的なチェック
public function show(Request $request, int $id)
{
$order = Order::findOrFail($id);
if ($order->user_id !== $request->user()->id) {
abort(403);
}
return $order;
}
パターン2: Laravel Policy を使った実装
Laravel には認可を管理する仕組み(Policy)が標準で用意されています。
// app/Policies/OrderPolicy.php
class OrderPolicy
{
public function view(User $user, Order $order): bool
{
return $user->id === $order->user_id;
}
}
// Controller
public function show(Request $request, int $id)
{
$order = Order::findOrFail($id);
$this->authorize('view', $order);
return $order;
}
Policy を使うことで、認可ロジックを一箇所に集約でき、
テストやメンテナンスがしやすくなります。
重要なのはコードの形ではなく、
- 一覧を経由しなくても安全か
- URL を直接叩かれても安全か
という視点です。
【注意】競合状態(Race Condition)にも気をつける
認可チェックには、もう一つ落とし穴があります。
それが 競合状態(Race Condition) です。
問題のあるコード
public function show(Request $request, int $id)
{
$order = Order::findOrFail($id);
// ここで認可チェック
if ($order->user_id !== $request->user()->id) {
abort(403);
}
// この間に order->user_id が変更される可能性
// (別プロセスで所有者変更など)
return $order;
}
何が問題か?
- データを取得
- 認可チェック
- データを使用
この間に、別のリクエストやバッチ処理でデータが変更される可能性があります。
これを TOCTOU(Time-of-check to time-of-use)問題 と呼びます。
対策
-
トランザクション内で取得とチェックを行う
-
楽観的ロック(Optimistic Locking)を使う
-
WHERE 句で直接絞り込む
// WHERE句で直接絞り込む例
$order = Order::where('id', $id)
->where('user_id', $request->user()->id)
->firstOrFail();
この方法なら、取得とチェックが1つのクエリで完結します。
まとめ
Broken Access Control は、
- 特定の技術の問題ではない
- 高度な攻撃は不要
- 最大の原因は「画面操作前提の思考」
です。
画面は UX を良くするためのもの。
セキュリティを保証するものではありません。
Broken Access Control に限ったことではないですが、
自分がやり方を知らない=誰もできない
ではないです。
自分はやり方知らないけど、もしこういうことがやられたら(できてしまったら) を常に意識することがセキュリティ対策の第一歩だと思います。
