「前端都驗證過了」是後端最危險的錯覺
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
這個操作在當下是否合法?
例如:
* 餘額是否足夠
* 是否超過每日限額
* 帳號是否被凍結
---
少掉任何一層,前端再怎麼驗證都救不了你。
> 永遠不要相信前端傳來的資料。
>
> 而且要分清楚:
>
> 你不信任的到底是資料格式、可修改欄位、計算結果,還是使用者身分。
點擊複製文章連結