WeHelp
「前端都驗證過了」是後端最危險的錯覺
2026-06-15 21:57:07
很多剛接觸後端開發的人,會有一個很自然的想法: > 前端都已經限制好了,使用者應該沒辦法亂輸入吧? 事實上,這是一個非常危險的觀念。 例如: ```json { "isAdmin": true } ``` 或是: ```json { "amount": -10000 } ``` 甚至: ```json { "memberId": 9527 } ``` 如果這些欄位不應該由使用者控制,但後端又沒有進行驗證,那麼系統就可能出現嚴重問題。 --- ## 使用者真的只能透過 UI 操作嗎? 答案是: **不是。** 前端 UI 只是正常使用者操作系統的一種方式。 攻擊者可以直接使用: * Postman * Burp Suite * curl * DevTools * 自己寫程式呼叫 API 完全繞過你的前端畫面。 例如前端根本沒有「管理員」選項: ```json { "username": "test", "password": "123456" } ``` 但攻擊者可以自行發送: ```json { "username": "test", "password": "123456", "isAdmin": true } ``` 如果後端沒有做好防護,就可能產生嚴重的權限漏洞。 --- ## 前端驗證不等於安全性 很多前端都會做一些資料檢查: * 必填欄位 * 長度限制 * Email 格式驗證 * 手機號碼格式驗證 例如: ```text 請輸入 Email 密碼至少 8 碼 ``` 這些功能很重要。 但它們的目的其實是: > 提升使用者體驗(UX) 而不是保護系統安全。 因為攻擊者根本不會經過你的畫面。 --- ## 後端必須驗證所有輸入資料 對後端來說: > Request 永遠只是一包來路不明的 JSON。 所以不管前端有沒有檢查過,後端都應該再次驗證。 常見的驗證內容包括: * 是否為必填欄位 * 是否符合格式 * 是否超出長度限制 * 是否為合法數值 * 是否超出允許範圍 --- ## FastAPI 的 Validation 在 FastAPI 中,最常見的做法是使用 Pydantic 進行資料驗證。 ```python from fastapi import FastAPI from pydantic import BaseModel, Field app = FastAPI() class User(BaseModel): username: str = Field(..., min_length=3, max_length=50) password: str = Field(..., min_length=8) @app.post("/register") async def register(user: User): return { "message": "User registered successfully" } ``` 這段程式碼可以保證: * username 不能為空 * username 長度必須介於 3 ~ 50 字元 * password 至少 8 碼 如果收到: ```json { "username": "", "password": "123" } ``` FastAPI 甚至還沒進入 API 邏輯,就會直接回傳錯誤。 這樣可以避免不合法的資料進入系統。 --- ## Validation 擋不住 Mass Assignment 回到一開始的例子: ```json { "username": "test", "password": "123456", "isAdmin": true } ``` 你可能會想: > 那我用 Validation 不就好了? 問題是: Validation 驗證的是: > 值是否合法 而不是: > 這個欄位該不該由使用者指定 `true` 本身是一個完全合法的布林值。 真正的問題在於: > isAdmin 根本不應該由使用者控制。 這類漏洞有個名字: **Mass Assignment(Over-posting)** 例如: ```python user = User(**request.dict()) ``` 如果 User 剛好包含: ```python isAdmin: bool ``` 攻擊者就可能直接幫自己升級權限。 --- ### 正確做法一:輸入 Model 與資料庫 Model 分離 ```python class RegisterRequest(BaseModel): username: str password: str ``` 不要把: ```python isAdmin createdAt balance role ``` 這些敏感欄位暴露給外部輸入。 --- ### 正確做法二:拒絕未知欄位 ```python from pydantic import BaseModel, ConfigDict class RegisterRequest(BaseModel): model_config = ConfigDict(extra="forbid") username: str password: str ``` 多送的欄位直接拒絕,或直接忽略,避免額外欄位影響系統行為。 --- ## 不要相信前端計算結果 另一個非常常見的錯誤: ```json { "price": 100, "quantity": 10, "total": 1000 } ``` 有些系統會直接相信前端算好的 total。 但攻擊者完全可以改成: ```json { "price": 100, "quantity": 10, "total": 1 } ``` 如果後端直接使用這個值: 恭喜。 原本 1000 元的商品變成 1 元。 --- 同樣的問題還包括: ```json { "discount": 99 } ``` ```json { "vipLevel": 999 } ``` ```json { "rewardPoint": 999999 } ``` 這些資料應該由後端自行計算。 而不是相信前端傳來的結果。 --- ## Validation 不等於商業邏輯驗證 例如: ```json { "amount": -10000 } ``` 可以透過: ```python amount: int = Field(..., gt=0) ``` 限制金額必須大於 0。 這屬於 Validation。 因為我們只是確認: > 資料格式是否合法。 --- 但下面這段: ```python if user.balance < amount: raise Exception("餘額不足") ``` 就不是 Validation。 而是 Business Logic。 因為它是在判斷: > 使用者是否符合執行這個行為的條件。 --- ## 還有一種驗證:你有沒有資格碰這筆資料? 再看另一個例子: ```json { "memberId": 9527 } ``` 格式完全正確。 Validation 不會有任何問題。 但問題是: > 9527 真的是你嗎? 如果使用者把 memberId 改成別人的: ```json { "memberId": 1001 } ``` 就可能看到別人的資料。 這種漏洞叫: **IDOR(Insecure Direct Object Reference)** 也是現實世界最常見的漏洞之一。 --- 它無法靠 Validation 解決。 因為資料本身沒有錯。 --- 授權真正要回答的問題是: > 這個已登入的人,有沒有資格存取這筆資源? 例如: ```python if member_id != current_user.id and not current_user.is_admin: raise HTTPException(status_code=403) ``` --- 更好的做法甚至是: ```http GET /me/orders ``` 而不是: ```http GET /members/{memberId}/orders ``` 直接從登入身份推導會員編號。 減少攻擊面。 --- ## 到底有哪些東西容易被搞混? 看到這裡,你可能會發現: 開頭那三個 JSON 看起來都很可疑。 但其實它們是三種完全不同的問題。 | 範例 | 問題類型 | | ---------------- | --------------- | | `amount: -10000` | Validation | | `isAdmin: true` | Mass Assignment | | `memberId: 9527` | Authorization | | `total: 1` | 不可信任的客戶端計算結果 | 它們看起來都像是: > 使用者傳來了奇怪的資料 但後面的防線完全不同。 --- ## 為什麼要把這些事情拆開? 很多專案最後都會變成: ```python if amount <= 0: ... if amount > 100000: ... if balance < amount: ... if isFrozen: ... if dailyLimitExceeded: ... ``` 所有規則全部塞進 Controller。 剛開始看起來沒問題。 但半年後: * 提款 * 轉帳 * VIP * 優惠活動 * 風控規則 全部混在一起。 系統就會越來越難維護。 當 Validation、Authorization、Business Logic 沒有分層時,程式通常也會開始失控。 --- ## 結語 Request 本質上只是一包來路不明的 JSON。 你不知道它是: * 瀏覽器送來的 * Postman 送來的 * Burp Suite 修改過的 * 還是攻擊者自己寫程式產生的 因此後端真正該做的,不是相信前端。 而是建立多層防線。 ### 第一層:Authentication 你是誰? 例如: * 帳號密碼 * JWT * OAuth --- ### 第二層:Validation 資料是否合法? 例如: * 必填欄位 * Email 格式 * 金額範圍 * 長度限制 --- ### 第三層:Mass Assignment 防護 哪些欄位允許被使用者設定? 例如: * isAdmin * role * balance 不應該由外部輸入控制。 --- ### 第四層:Authorization 你能碰哪些資源? 例如: * 是否能查看這筆訂單 * 是否能修改這個會員資料 * 是否能存取這個 API --- ### 第五層:Business Logic 這個操作在當下是否合法? 例如: * 餘額是否足夠 * 是否超過每日限額 * 帳號是否被凍結 --- 少掉任何一層,前端再怎麼驗證都救不了你。 > 永遠不要相信前端傳來的資料。 > > 而且要分清楚: > > 你不信任的到底是資料格式、可修改欄位、計算結果,還是使用者身分。