Lesson 11: 應用層的 Zero Trust-資料驗證 (Validation) 與 DTO
在資安領域有一個非常有名的概念叫做 Zero Trust (零信任)。它的核心精神只有一句話:"Never Trust, Always Verify" (永不信任,始終驗證)。
在網路世界裡,後端工程師的第一條守則是:「永遠不要相信客戶端傳來的輸入」。
過去工程師常認為:「只要使用者登入了(通過驗證),他傳來的資料就是安全的」或者「這是我們自己寫的前端 App 傳來的資料,所以可以信任」。這就是傳統的邊界防禦 (Perimeter Security) 思維——就像城堡,進了城門就沒人管你了。
但對於後端工程師來說,API 接口就是戰場的最前線,沒有所謂的「城牆內」。
無論前端做了多完美的檢查,惡意攻擊者依然可以用 curl 或 Postman 直接對你的 API 灌入垃圾數據或攻擊代碼。這堂課我們探討如何建立一道堅固的防火牆,確保進入我們系統核心(業務邏輯與資料庫)的數據是乾淨、合法且型別正確的
為什麼要用 Zero Trust 看待輸入資料?
將 Zero Trust 的哲學應用在資料驗證上,我們可以這樣理解:
傳統思維:
「前端有寫 JavaScript 檢查 Email 格式了,後端不用再檢查一次吧?」
「這是內部管理後台,只有員工會用,不用防 SQL Injection 吧?」
後果:一旦前端驗證被繞過(攻擊者直接用 Postman 打 API),後端完全不設防,直接讓惡意資料長驅直入。
零信任思維 (Zero Trust Security):
假設前端已經淪陷:我們假設發送請求的不是你的前端 App,而是一個正在嘗試注入惡意語法的駭客。
身份不代表清白:即使請求帶有合法的 JWT (Token),代表他是「合法的使用者」,但不代表他帶來的資料是「無害的」。
微觀邊界 :每一個 API Endpoint 都是一個獨立的檢查站。不管資料從哪裡來,進入這個函式之前,必須先經過嚴格的驗證。
DTO (Data Transfer Object)
在我的開發生涯中,曾經看過有工程師 直接把 HTTP Request 的 JSON Body 整個丟進資料庫模型(Model)裡。這是極度危險的行為(大量指派漏洞)。
DTO 模式 的核心思想是:建立一個專門的物件,用來定義「這個 API 接口預期接收什麼樣的資料」。
沒有 DTO (危險):
客戶端傳送
{ "username": "admin", "role": "superuser" }。後端直接接收並寫入資料庫 -> 一般使用者突然變成了超級管理員。
使用 DTO (安全):
後端定義一個
UpdateProfileDTO,裡面只有{ "username": string }。即使客戶端多傳了
role,因為 DTO 裡沒定義,系統會自動忽略或報錯。
驗證的三個層次
一個成熟的後端架構,驗證通常發生在不同階段:
Layer 1: 結構與型別驗證 (Syntactic Validation)
這是最外層的過濾,通常由 Schema Validator 處理。如果不通過,程式根本不應該進入業務邏輯。
檢查項目:
欄位是否存在?(Required)
資料型別對不對?(Is it a String, Integer, UUID?)
格式對不對?(Email format, Date format ISO8601)
通用工具:
Python: Pydantic
Node.js/TS: Zod, Joi, class-validator
Go: Struct Tags (
binding:"required,email")JSON Schema: 跨語言的標準定義
Layer 2: 語意與邏輯驗證 (Semantic Validation)
這層涉及資料的「內容」是否合理。
檢查項目:
數值範圍:年齡不能是負數,庫存不能小於 0。
依賴關係:如果
payment_method是信用卡,則card_number必填。商業規則:開始時間不能晚於結束時間。
Layer 3: 資料庫完整性驗證 (Database Integrity)
這是最後一道防線,通常涉及對資料庫的查詢。
檢查項目:
唯一性檢查 (Unique):這個 Email 是否已經被註冊過?
關聯性檢查 (Foreign Key):這個
product_id是否真的存在於商品表中?
Fail Fast 原則
後端驗證的核心哲學是 Fail Fast (快速失敗)。 一旦發現資料不合法,立刻回傳 400 Bad Request 或 422 Unprocessable Entity,不要讓錯誤的資料繼續往後跑,浪費運算資源或導致更深層的錯誤(例如資料庫報錯)。
補充
關於DTO
DTO , Data transfer(to) Object。本質是「資料傳輸的載體」,它不只用來「收(Input)」資料,也非常適合用來「發(Output)」資料。
我們上面談了 DTO 如何像盾牌一樣防禦惡意輸入 (Input DTO),但 DTO 還有另一個強大的功能:規範輸出的形狀 (Output DTO / Response DTO)。
如果我們直接把資料庫撈出來的 Model 直接 return 給前端或第三方。這會導致兩個問題:
洩漏敏感資料:不小心把
password_hash、soft_delete_flag或內部id傳出去。格式不符需求:資料庫存的是
created_at(Timestamp),但對方要的是 XML 格式的<publishedDate>。
實戰場景:整個電商網站商品 XML Feed
假設你需要生成一個 XML 檔案給 Google Merchant Center 或比價網爬蟲使用。你的資料庫結構可能很複雜,但對方要求的格式很死板。這時,你可以用 DTO 來做「結構化映射」。
情境:
資料庫 (DB):
products表,欄位有id,name,cost_price,sale_price,stock_qty,supplier_id。需求 (XML):需要
<item><title>...</title><price>...</price></item>。
使用 Output DTO 的思維:
// 這是我們定義的「模具」,不管資料庫怎麼變,輸出的格式永遠長這樣
class ProductXmlDTO
{
public string $title;
public float $price;
public string $availability;
// 透過建構子或工廠方法,把 DB Model 轉換成 DTO
public static function fromModel(Product $product): self
{
$dto = new self();
// 1. 改名 (Mapping):DB 的 name 對應 XML 的 title
$dto->title = $product->name;
// 2. 邏輯轉換 (Transformation):DB 存的是成本與售價,這裡只輸出最終售價
$dto->price = $product->sale_price;
// 3. 狀態判斷 (Logic):將數字庫存轉為文字描述
$dto->availability = $product->stock_qty > 0 ? 'in stock' : 'out of stock';
return $dto;
}
}
這樣做的好處
解耦 (Decoupling):如果有一天你的資料庫欄位
name改成了product_name,你只需要修改 DTO 的fromModel方法,外面的 XML 輸出完全不會壞掉。單一資料來源 (SSOT):你可以針對不同的需求製作不同的 DTO(例如
ProductCardDTO給手機版列表用,ProductDetailDTO給詳情頁用),而不是讓前端自己去撈一堆不需要的欄位。
大量指派漏洞(Mass Assignment Vulnerability)
這裡我們來談談什麼是大量指派漏洞,用一個具體情境來說明:
你有一個「更新使用者資料」的功能,讓使用者編輯自己的個人資訊(profile)。
你提供一個
PATCH/PUTAPI 供前端送出更新請求。在 users table 中,你有一個
remember_token欄位,用於實現「持久登入」。客戶端在呼叫更新 API 時,body 中不小心也包含了
remember_token。後端程式如果直接使用
$request->all()去更新資料模型(User::update($request->all())),那麼remember_token就會在你完全未察覺的情況下被改寫。
這就是典型的 大量指派漏洞:
攻擊者或不小心的客戶端,只要傳入一個你沒預期要更新的欄位,就能成功覆寫你的資料。

