本文 AI 產出,尚未審核
FastAPI 課程 – 安全性:CSRF 保護
簡介
在 Web 應用程式中,跨站請求偽造(Cross‑Site Request Forgery,簡稱 CSRF) 是常見且危險的攻擊手法。攻擊者藉由使用者已登入的身分,在不知情的情況下替使用者發送惡意請求,可能導致資料竄改、帳號被盜或金錢交易被竊。
FastAPI 本身是基於 Starlette 的非同步框架,雖然它提供了強大的驗證與授權機制(OAuth2、JWT 等),但對於 CSRF 的防護仍需要開發者自行設計或使用第三方套件。了解 CSRF 的原理、正確的防護方式,才能確保 API 在前端(尤其是瀏覽器)呼叫時不會被惡意利用。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步在 FastAPI 中完成 CSRF 防護,適合初學者到中階開發者快速上手。
核心概念
1. CSRF 的運作機制
| 步驟 | 說明 |
|---|---|
| 1️⃣ 使用者登入網站,取得 Session Cookie(或 JWT) | 瀏覽器自動在同源請求中附帶此 Cookie。 |
2️⃣ 攻擊者在其他網站放置一段惡意表單或 <img> 標籤 |
目的是讓使用者在不知情的情況下發送請求到受害網站。 |
| 3️⃣ 瀏覽器因同源政策會自動攜帶受害網站的 Cookie | 伺服器端無法分辨請求是合法還是偽造的。 |
| 4️⃣ 若受害網站沒有檢查 CSRF Token,請求成功執行 | 攻擊者即可完成資料竄改或其他惡意行為。 |
重點:CSRF 不是偷取資料,而是利用使用者已授權的身份「冒充」使用者發送請求。
2. 防禦原則
- 同源檢查(SameSite Cookie)
- 設定 Cookie 的
SameSite為strict或lax,讓瀏覽器在跨站請求時不自動攜帶 Cookie。
- 設定 Cookie 的
- CSRF Token
- 伺服器在產生表單或 API 回應時,同時產生一個隨機的 token,前端必須在每次變更資料的請求中攜帶此 token(通常放在
X-CSRF-Token標頭或表單隱藏欄位)。
- 伺服器在產生表單或 API 回應時,同時產生一個隨機的 token,前端必須在每次變更資料的請求中攜帶此 token(通常放在
- 雙重驗證(Double Submit Cookie)
- 把 token 同時放在 Cookie 與請求標頭,伺服器比對兩者是否一致。
在 FastAPI 中,我們常採用 雙重驗證 的方式,因為它不需要在每個表單頁面手動注入 token,且適用於純 API(SPA)架構。
3. FastAPI 與 Starlette 的中介層(Middleware)
FastAPI 允許自訂 Middleware,在請求進入路由前先執行檢查。利用 Middleware,我可以在每一次 POST、PUT、PATCH、DELETE 請求時,自動驗證 CSRF token。
以下示範三種常見的實作方式:
- 方式 A:使用
SameSiteCookie(最簡單的防護) - 方式 B:自行撰寫 CSRF Middleware(雙重驗證)
- 方式 C:結合第三方套件
fastapi-csrf-protect
程式碼範例
範例 1️⃣ – 設定 SameSite Cookie
# main.py
from fastapi import FastAPI, Response
app = FastAPI()
@app.get("/login")
def login(response: Response):
# 假設這裡已完成驗證,發放 session_id
response.set_cookie(
key="session_id",
value="abc123def456",
httponly=True,
samesite="strict", # ← 防止跨站攜帶
secure=True,
)
return {"message": "已登入,Cookie 已設定為 SameSite=Strict"}
說明:
samesite="strict"讓瀏覽器在任何跨站請求(包括第三方圖片、表單)時都不會攜帶session_id,從根本上阻斷大部分 CSRF。- 若需要允許從外部連結點擊進入(如搜尋結果),可改為
lax,但仍能防止 GET 以外的跨站請求。
範例 2️⃣ – 雙重驗證 Middleware(自行實作)
# csrf_middleware.py
import secrets
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
CSRF_COOKIE_NAME = "csrf_token"
CSRF_HEADER_NAME = "X-CSRF-Token"
class CSRFMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 只檢查會改變資料的 HTTP 方法
if request.method in ("POST", "PUT", "PATCH", "DELETE"):
# 1. 從 Cookie 取得 token
cookie_token = request.cookies.get(CSRF_COOKIE_NAME)
# 2. 從標頭取得 token
header_token = request.headers.get(CSRF_HEADER_NAME)
if not cookie_token or not header_token or cookie_token != header_token:
return JSONResponse(
{"detail": "CSRF token invalid or missing"},
status_code=403,
)
# 3. 若是安全的 GET 請求,若沒有 token 就產生一個
response = await call_next(request)
if request.method == "GET" and CSRF_COOKIE_NAME not in request.cookies:
new_token = secrets.token_urlsafe(32)
response.set_cookie(
key=CSRF_COOKIE_NAME,
value=new_token,
httponly=False, # 必須讓前端 JavaScript 能讀取
samesite="lax",
secure=True,
)
return response
# main.py
from fastapi import FastAPI
from csrf_middleware import CSRFMiddleware
app = FastAPI()
app.add_middleware(CSRFMiddleware)
@app.get("/form")
def get_form():
# 前端會從 cookie 讀取 csrf_token,放入表單或 header
return {"message": "取得表單,請在 POST 時帶上 X-CSRF-Token"}
@app.post("/update")
def update_item():
# 若 token 驗證失敗,上面的 middleware 已回傳 403
return {"detail": "資料已更新"}
說明:
- GET 請求時若沒有 CSRF token,會自動產生並寫入 Cookie。
- 前端(例如 Vue、React)需要從
document.cookie讀取csrf_token,再放入每次變更資料的 標頭X-CSRF-Token。httponly=False讓 JavaScript 能存取 token;若擔心 XSS,可考慮使用 Content Security Policy 限制腳本來源。
範例 3️⃣ – 使用 fastapi-csrf-protect 套件(簡化版)
pip install fastapi-csrf-protect
# main.py
from fastapi import FastAPI, Depends, Request
from fastapi_csrf_protect import CsrfProtect
from pydantic import BaseSettings
class Settings(BaseSettings):
secret_key: str = "超級隨機的密鑰"
app = FastAPI()
@CsrfProtect.load_config
def get_config():
return Settings()
@app.get("/csrf-token")
def get_csrf(csrf_protect: CsrfProtect = Depends()):
# 產生 token 並存入 cookie
token = csrf_protect.generate_csrf()
response = {"msg": "CSRF token 已設定於 cookie"}
csrf_protect.set_csrf_cookie(response)
return response
@app.post("/secure-action")
def secure_action(request: Request, csrf_protect: CsrfProtect = Depends()):
# 直接在路由內驗證 token
csrf_protect.validate_csrf_in_cookies(request)
return {"detail": "安全操作完成"}
說明:
fastapi-csrf-protect已封裝好雙重驗證的流程,開發者只要在需要的路由加上csrf_protect.validate_csrf_in_cookies即可。- 只要前端在每次 POST/PUT/PATCH/DELETE 時把
X-CSRF-Token(或從 cookie 讀取)帶上,就能通過驗證。
範例 4️⃣ – 前端 (JavaScript) 如何攜帶 CSRF Token
// utils/csrf.js
export function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? decodeURIComponent(match[2]) : null;
}
export async function postWithCsrf(url, data) {
const token = getCookie('csrf_token'); // 或根據套件名稱調整
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token, // 必須與後端設定的 Header 名稱一致
},
body: JSON.stringify(data),
credentials: 'include', // 讓瀏覽器帶上 cookie
});
return response.json();
}
說明:
credentials: 'include'確保瀏覽器在跨域情況下仍會送出 Cookie(前提是 CORS 設定允許)。- 若使用
fastapi-csrf-protect,Header 名稱預設是X-CSRF-Token,可自行在套件設定中修改。
範例 5️⃣ – 在 CORS 中同時允許 CSRF Header
# main.py (續)
from fastapi.middleware.cors import CORSMiddleware
origins = [
"https://frontend.example.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True, # 必須允許 credentials 才會送 cookie
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["*"], # 或明確列出 "X-CSRF-Token"
)
說明:
- 若前端與後端分屬不同子域,必須在 CORS 中開放
allow_credentials=True,否則即使 token 正確,瀏覽器也不會送出 Cookie,導致驗證失敗。
常見陷阱與最佳實踐
| 陷阱 | 為何會發生 | 解決方式 |
|---|---|---|
| 忘記在 GET 回應中設定 CSRF token | 前端在第一次載入頁面時無 token,後續 POST 失敗。 | 在 Middleware 或專門的 /csrf-token 路由中自動產生並寫入 Cookie。 |
| 把 token 設為 HttpOnly | 前端無法讀取 token,無法在標頭中帶出。 | 不要 把 CSRF token 設為 HttpOnly,只把 session cookie 設為 HttpOnly。 |
| CORS 未允許 X‑CSRF‑Token 標頭 | 前端發送的自訂標頭被瀏覽器阻擋,導致 403。 | 在 CORSMiddleware 中加入 allow_headers=["X-CSRF-Token"](或 allow_headers=["*"])。 |
| 使用 SameSite=Lax 而忘記保護 POST 請求 | Lax 仍允許跨站的 GET 請求,若後端允許 GET 變更資料會有風險。 | 永遠 使用 POST、PUT、PATCH、DELETE 來執行資料變更,並在這些方法上檢查 token。 |
| Token 產生不夠隨機 | 攻擊者可猜測 token,繞過防護。 | 使用 secrets.token_urlsafe(32) 或類似高熵隨機函式產生 token。 |
最佳實踐
- 同時使用 SameSite 與 CSRF Token
- SameSite 可作為第一道防線,CSRF Token 為第二層保護。
- 在每次 Session 建立時重新產生 CSRF token
- 防止舊 token 被竊取後長時間有效。
- 將 CSRF token 置於短期 Cookie(例如 30 分鐘)
- 減少被盜用的時間窗口。
- 結合 CSP(Content Security Policy)與 XSS 防護
- CSRF token 若被 XSS 竊取,仍會失效。
- 在測試環境加入自動化 CSRF 測試
- 使用
pytest搭配httpx,確保所有變更資料的端點都有正確的 token 驗證。
- 使用
實際應用場景
| 場景 | 為何需要 CSRF 防護 | 建議實作方式 |
|---|---|---|
| 單頁應用(SPA)使用 JWT | 前端會把 JWT 放在 Authorization 標頭,瀏覽器仍會送出 Cookie(若有)造成偽造請求。 |
使用 雙重驗證:在 JWT 登入成功後,後端同時回傳 CSRF token,前端存入 Cookie 並在每次變更請求帶上 X-CSRF-Token。 |
| 傳統表單提交的企業內部系統 | 大多數使用 POST 表單,且會在同一網域內運行。 | 只需要 SameSite=Strict 即可;若需跨子域,使用 雙重驗證 並在表單隱藏欄位加入 token。 |
| 跨域的微服務 API | 前端與 API 分屬不同子域,且使用 CORS。 | 在 API 閘道(API Gateway)層加入 CSRFMiddleware,同時在 CORS 設定 allow_credentials=True 與 allow_headers=["X-CSRF-Token"]。 |
| 行動 App 呼叫 FastAPI | 行動端不會自動帶 Cookie,通常使用 JWT。 | CSRF 風險較低,可不必實作 token;但若同時支援 Web 端,仍建議在 Web 端啟用 CSRF 防護。 |
總結
- CSRF 是利用已登入使用者的身份發送未授權請求的攻擊,對任何使用 Cookie(或其他自動攜帶認證)的 Web 應用都有威脅。
- 在 FastAPI 中,我們可以透過三層防護:
- SameSite Cookie(第一道防線)
- 雙重驗證的 CSRF token(核心防護)
- CORS + CSP + XSS 防護(輔助保護)
- 實作上,可以自行撰寫 Middleware,或直接使用成熟套件
fastapi-csrf-protect,兩者皆能在 POST、PUT、PATCH、DELETE 等會改變資料的請求中自動驗證 token。 - 別忘了在前端正確取得 token(從 cookie)並放入
X-CSRF-Token標頭,同時在 CORS 中允許此自訂標頭與 credentials。 - 最後,結合測試、自動化與安全政策,才能在開發、測試、上線全流程中維持 CSRF 防護的完整性。
掌握以上概念與實作技巧,你的 FastAPI 專案將能在面對跨站請求偽造時,提供堅實且易於維護的安全防線。祝開發順利!