本文 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. 防禦原則

  1. 同源檢查(SameSite Cookie)
    • 設定 Cookie 的 SameSitestrictlax,讓瀏覽器在跨站請求時不自動攜帶 Cookie。
  2. CSRF Token
    • 伺服器在產生表單或 API 回應時,同時產生一個隨機的 token,前端必須在每次變更資料的請求中攜帶此 token(通常放在 X-CSRF-Token 標頭或表單隱藏欄位)。
  3. 雙重驗證(Double Submit Cookie)
    • 把 token 同時放在 Cookie 與請求標頭,伺服器比對兩者是否一致。

在 FastAPI 中,我們常採用 雙重驗證 的方式,因為它不需要在每個表單頁面手動注入 token,且適用於純 API(SPA)架構。


3. FastAPI 與 Starlette 的中介層(Middleware)

FastAPI 允許自訂 Middleware,在請求進入路由前先執行檢查。利用 Middleware,我可以在每一次 POST、PUT、PATCH、DELETE 請求時,自動驗證 CSRF token。

以下示範三種常見的實作方式:

  • 方式 A:使用 SameSite Cookie(最簡單的防護)
  • 方式 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": "資料已更新"}

說明

  1. GET 請求時若沒有 CSRF token,會自動產生並寫入 Cookie。
  2. 前端(例如 Vue、React)需要從 document.cookie 讀取 csrf_token,再放入每次變更資料的 標頭 X-CSRF-Token
  3. 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。

最佳實踐

  1. 同時使用 SameSite 與 CSRF Token
    • SameSite 可作為第一道防線,CSRF Token 為第二層保護。
  2. 在每次 Session 建立時重新產生 CSRF token
    • 防止舊 token 被竊取後長時間有效。
  3. 將 CSRF token 置於短期 Cookie(例如 30 分鐘)
    • 減少被盜用的時間窗口。
  4. 結合 CSP(Content Security Policy)與 XSS 防護
    • CSRF token 若被 XSS 竊取,仍會失效。
  5. 在測試環境加入自動化 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=Trueallow_headers=["X-CSRF-Token"]
行動 App 呼叫 FastAPI 行動端不會自動帶 Cookie,通常使用 JWT。 CSRF 風險較低,可不必實作 token;但若同時支援 Web 端,仍建議在 Web 端啟用 CSRF 防護。

總結

  • CSRF 是利用已登入使用者的身份發送未授權請求的攻擊,對任何使用 Cookie(或其他自動攜帶認證)的 Web 應用都有威脅。
  • FastAPI 中,我們可以透過三層防護:
    1. SameSite Cookie(第一道防線)
    2. 雙重驗證的 CSRF token(核心防護)
    3. CORS + CSP + XSS 防護(輔助保護)
  • 實作上,可以自行撰寫 Middleware,或直接使用成熟套件 fastapi-csrf-protect,兩者皆能在 POST、PUT、PATCH、DELETE 等會改變資料的請求中自動驗證 token。
  • 別忘了在前端正確取得 token(從 cookie)並放入 X-CSRF-Token 標頭,同時在 CORS 中允許此自訂標頭與 credentials。
  • 最後,結合測試、自動化與安全政策,才能在開發、測試、上線全流程中維持 CSRF 防護的完整性。

掌握以上概念與實作技巧,你的 FastAPI 專案將能在面對跨站請求偽造時,提供堅實且易於維護的安全防線。祝開發順利!