Learning Objectives
- 分辨 401 與 403,知道各自什麼時候用
- 找出並修好「改網址就看到別人資料」的漏洞(IDOR)
- 用裝飾器寫出角色檢查 require_role
- 說明為什麼隱藏按鈕不算權限,以及為什麼不能相信表單傳來的 user_id
登入之後還有一個問題
上一課確認了「你是誰」(驗證,authentication)。這一課處理「你可以做什麼」(授權,authorization)。
兩件事常被混在一起,但檢查的東西不同:
| 問題 | 沒通過時的狀態碼 | |
|---|---|---|
| 驗證 | 你是誰? | 401:伺服器不知道你是誰,請先登入 |
| 授權 | 你可以做這件事嗎? | 403:知道你是誰,但你沒有這個權限 |
很多網站只做了前面一半:只要登入,什麼都看得到。OWASP Top 10:2025 把存取控制失效(Broken Access Control)列為第一項,這類問題非常普遍,而且不需要特殊工具,改一下網址就能試。
逐步看:401、403 與 200
同一個網址,由沒登入的人、學生、老師各開一次。注意 wrapped 在哪一行就結束了,grades 這個 view 函數只有一次被呼叫。
401 與 403 在實務上怎麼用
- API:照上面的定義回 401 或 403,由前端決定怎麼處理。依 HTTP 規格,401 要附上
WWW-Authenticate標頭說明認證方式;Flask 的abort(401)不會自動加,正式的 API 要自己補。 - 給人看的網頁:沒登入時通常直接轉到登入頁(302),就是上一課
login_required的做法。已登入但權限不足時回 403,並放一個自訂的錯誤頁(第 7 課)。
角色怎麼存?最簡單的做法是 users 資料表多一個 role 欄位,每次請求從資料庫查。如果把角色放在 session,老師把某人的權限拿掉之後,對方的 Cookie 裡還是舊的角色。
下面看一個更隱蔽的問題:大家都是學生,角色一樣,資料卻各是各的。
逐步看:改網址就看到別人的筆記
Amy(編號 1)依序開三種版本的網址。比較每個版本的 SQL,以及 Ben 的筆記在哪個版本被擋下來。
回 403 還是 404
剛才的修法查不到就回 404。另一種寫法是先查出來、比對後回 403。兩種都擋得住,差別在於要不要讓對方知道這筆資料存在:
| 回 403 | 回 404 | |
|---|---|---|
| 對方知道什麼 | 這個編號有資料,只是不屬於你 | 分不出是不存在還是不屬於你 |
| 適合 | 資料的存在本身不敏感,例如課程頁面,或需要提示「請向老師申請權限」 | 私人資料,例如筆記、訊息、成績。OWASP 建議這類情況對「不存在」與「無權限」回相同的回應 |
另外,把編號換成不容易猜的 UUID 可以提高猜中的難度,但不能取代檢查:網址一旦被分享或外流,對方還是打得開。OWASP 也提醒,把編號藏起來不算足夠的防護,每個請求都要檢查。
逐步看:表單傳來的 user_id 不能信
表單裡常有 <input type="hidden" name="user_id">。隱藏欄位只是畫面上看不到,送出前一樣改得了。下面的程式就是直接送一個別人的編號。
動手做:只看得到自己的筆記
完成 note:沒登入回 401;查詢同時比對編號與登入者,所以別人的筆記和不存在的筆記都回 404。SQL 一樣用 ? 當參數。程式最下面的測試不要改。
想一想
一位已登入的學生打開只有老師能看的成績頁。比較適當的狀態碼是?
下面程式登入的是編號 1,執行後會印出什麼?
from flask import Flask, session, abort
app = Flask(__name__)
app.secret_key = 'dev'
notes = {1: 1, 2: 2} # 筆記編號對應擁有者
@app.route('/login')
def login():
session['user_id'] = 1
return 'ok'
@app.route('/notes/<int:nid>')
def note(nid):
if notes.get(nid) != session.get('user_id'):
abort(404)
return '筆記'
client = app.test_client()
client.get('/login')
print(client.get('/notes/1').status_code, client.get('/notes/2').status_code, client.get('/notes/9').status_code)
編輯筆記的表單裡有 <input type="hidden" name="user_id" value="1">,後端用 request.form['user_id'] 決定要改誰的筆記。問題是?
老師的刪除按鈕用 {% if role == 'teacher' %} 包起來,後端路由沒有其他檢查。學生能不能刪除?
課程的私人訊息頁,要回答「這筆訊息不是你的」。下面哪個說法最恰當?
重點整理
- 401 是不知道你是誰,403 是知道你是誰但沒有權限
- 登入不等於有權限:每個讀取或修改單筆資料的路由,都要檢查這筆資料是不是你的(IDOR)
- 最簡單的修法是把擁有者寫進查詢:
WHERE id = ? AND user_id = ? - 私人資料查不到就回 404,不透露資料是否存在;存在本身不敏感時可以回 403
- 角色用
require_role裝飾器集中檢查,角色從資料庫查 - 按鈕依權限顯示只是介面;檢查要寫在後端,依據 session 與資料庫,不信任表單傳來的 user_id
這一課的攻擊者是登入的使用者。下一課看來自外面的攻擊:SQL 注入、XSS 與 CSRF。