3.3 授權漏洞:IDOR 與越權訪問#
認證通過 ≠ 授權正確#
認證(你是誰)與授權(你能做什麼)的混淆是 Web 漏洞的常青問題。登入成功只回答前者;以下攻擊全部發生在「合法登入的使用者」身上。
IDOR:改個數字就看別人的資料#
IDOR(Insecure Direct Object Reference,2013 年進 OWASP Top 10 並成為 Bug Bounty 最常見獎項):
GET /api/orders/10001 ← 我的訂單
GET /api/orders/10002 ← 改個數字 → 別人的訂單??
flowchart LR
A[合法登入者] --> B[請求 /orders/10002]
B --> C{伺服器只驗登入\n沒驗所有權?}
C -->|是| D[回傳他人訂單 危險]
C -->|否| E[404/403]防禦:所有權檢查下沉到資料層#
回顧前書 5.1 的做法:
UPDATE todos SET done = NOT done
WHERE id = $1 AND user_id = $2 ← 授權即查詢條件
// 或在 repo 層統一封裝
pub async fn find_user_todo(pool: &PgPool, id: Uuid, user_id: Uuid) -> Option<Todo> {
sqlx::query_as!(Todo, "SELECT * FROM todos WHERE id = $1 AND user_id = $2", id, user_id)
.fetch_optional(pool).await.ok().flatten()
}
把「owner 欄位 = 當前使用者」變成所有查詢的強制條件 —— 不存在「先查再檢查」的時間差與遺漏點。這是結構性防禦 vs 紀律性防禦的分野。
UUID 能防 IDOR 嗎?#
不可枚舉的 id(UUID)讓「改數字」變難,但:
- id 會出現在分享連結、日誌、Referer
- 隱藏 ≠ 授權(1.1 的 obscurity 教訓)
結論:UUID 當輔助,所有權檢查不可省。
授權模型:三種正統#
1. RBAC(角色制)#
enum Role { User, Editor, Admin }
fn can(role: Role, action: Action) -> bool {
use Role::*;
match (role, action) {
(Admin, _) => true,
(Editor, Action::Publish | Action::Edit) => true,
(User, Action::Read) => true,
_ => false,
}
}
適合權限有限的組織系統。弱點:角色爆炸(super-admin-2024-final-v2)。
2. ABAC(屬性制)#
\[ \text{允許} \iff f(\text{主體屬性}, \text{資源屬性}, \text{環境屬性}) \]
例:「文檔建立者的同部門成員、在上班時間、可讀」。靈活但難審計。
3. 關係型(ReBAC,Google Zanzibar 2019)#
「文件的協作者、協作者的同事」—— 權限是關係圖。Google Drive 的分享模型、當代授權服務(SpiceDB、OpenFGA)的基礎。
\[ \text{選型}: \text{角色少選 RBAC}, \text{規則複雜選 ABAC}, \text{共享關係複雜選 ReBAC} \]
權限提升的其他路徑#
| 攻擊 | 例 | 防禦 |
|---|---|---|
| 水平越權 | 改 id 看別人(IDOR) | owner 條件 |
| 垂直越權 | 一般用戶呼叫 admin API | 端點級角色中介層 |
| 隱藏端點 | 「不顯示按鈕」但 API 還在 | UI 隱藏 ≠ 權限控制 |
| 參數信任 | req.body.role = "admin" 被接受 | 白名單欄位(zod/提取器) |
| 功能級遺漏 | 忘記給新端點掛中介層 | 預設拒絕(deny by default)路由設計 |
// 預設拒絕:router 分組 + 整組中介層(前書 3.2)
let admin = Router::new()
.route("/admin/users", get(list_all))
.route_layer(middleware::from_fn(require_admin)); // 忘記掛的端點不會誤入此組
預設拒絕是授權版的「secure by default」:忘記授權的結果是「不能做」而非「能做」。
批量賦能(Mass Assignment)#
// 把整個 body 丟給 update —— 危險
await User.update(req.user.id, req.body);
// body 混入 "role": "admin" → 自我升權!
// 白名單欄位
const { name, email } = req.body; // 只取允許的
await User.update(req.user.id, { name, email });
與 2.1 XSS 同根源:相信了不可信資料的形狀。zod/提取器(前書 3.2)的「只接受宣告過的欄位」是同一防禦。
習題#
- 審查你所有 API:列出哪些端點缺 owner 條件或角色中介層,逐一修復。
- 設計「文件共享」的 ReBAC 資料模型(tables + 查詢)。
- 為什麼「前端隱藏按鈕」完全不構成防禦?從攻擊者的工具(curl)角度說明。