# 安全中介層全家桶：CORS / CSRF / JWT / rate limit / binder

> gortex 在框架層整合 body 上限、CORS、CSRF、JWT、rate limit 與 log 遮蔽：JWT 鎖定 HS256，按 IP 做 rate limit 要配 TrustedProxies 才算數。

原文：https://sheng.page/posts/gortex-security-middleware-suite/ · 發布：2026-06-13 · 作者：Sheng

安全靠一整套 middleware，單一個擋不住。gortex 把 body 上限、CORS、CSRF、JWT、rate limit、log 遮蔽整合進框架層，業務端不必每次重配。這篇逐個拆。

> 前置閱讀：〈[net/http 與 middleware 模式](/posts/go-net-http-and-middleware/)〉

## Binder：body 上限

最直接的不可信輸入就是 request body。Binder 把 JSON body 卡在 1 MiB（`DefaultMaxJSONBodyBytes = 1 << 20`），而且用 `http.MaxBytesReader` 在讀的當下就擋，超過上限直接報錯，不會先把整包灌進記憶體：

```go
const DefaultMaxJSONBodyBytes int64 = 1 << 20 // 1 MiB
limited := http.MaxBytesReader(c.Response(), originalBody, limit)
// 解碼 limited；body 超過 limit 會在這裡失敗，而不是 OOM
```

multipart 另有上限（依框架文件為 32 MiB）。這是「把不可信輸入卡在邊界」最基本的一招，擋掉 oversize-body 的 DoS。

## CORS / CSRF

CORS：`CORSHandlerWithConfig` 依 `CORSConfig` 設 `Access-Control-Allow-Origin` / `-Methods` / `-Credentials` 等標頭，並補 `Vary`。注意預設 `AllowOrigins` 是 `["*"]`，方便開發，上線該收緊成白名單。能在框架層改一次預設、所有服務一致，正是〈[為什麼造一個 kitchen-sink 框架](/posts/gortex-why-kitchen-sink-framework/)〉講的依賴集中管理。

CSRF：用 double-submit token。產一個隨機 token（預設 32 bytes）放 cookie，client 必須在 header 回傳同一個；對不上是 `ErrCSRFTokenMismatch`、沒帶是 `ErrCSRFTokenMissing`，都回 403。cookie 的 `SameSite` 預設 Lax，安全路由可用 `Skipper` 跳過。

## JWT：32-byte、HS256、typ

JWT 這塊有三道刻意的硬規則：

1. **secret ≥ 32 bytes**：`NewJWTService` 對短於 `MinJWTSecretBytes`（32，對齊 SHA-256 輸出長度）的 secret 直接回 `ErrJWTSecretTooShort`；`config.Validate` 在載入設定時就擋。短 key 會弱化 HMAC。
2. **HS256-only**：驗證時 `keyFuncHS256` 檢查 `token.Method == jwt.SigningMethodHS256`，連 HS384/HS512 都拒。

```go
func (s *JWTService) keyFuncHS256(token *jwt.Token) (any, error) {
    if token.Method != jwt.SigningMethodHS256 {
        return nil, ErrUnexpectedSigningMethod // 擋演算法混淆攻擊
    }
    return s.secret, nil
}
```

   為什麼重要：只用 `*jwt.SigningMethodHMAC` 做斷言會連帶接受其他 HMAC 演算法，是經典的 algorithm-confusion 漏洞。鎖死 HS256 才安全。
3. **typ claim**：token 帶 `typ`（`access` / `refresh`）。把 refresh token 拿去當 access 用會回 `ErrInvalidTokenType`；舊版沒有 `typ` 的 token 一律拒收。

## Rate limit 與 TrustedProxies

rate limiter 用 token bucket 對每個 IP 做 rate limit，背景 goroutine 定期清過期 entry，並送出 `X-RateLimit-Limit` / `-Remaining` / `-Reset`，超限時加 `Retry-After`。

但「按 IP 做 rate limit」的前提是 IP 可信。client 可以隨手塞一個假的 `X-Forwarded-For` 來換 IP、繞過 rate limit。所以要配 `TrustedProxies`（一組 CIDR）：只有來自信任 proxy 的請求，`X-Forwarded-For` / `X-Real-IP` 才採信；否則忽略轉發標頭、用真正的 `RemoteAddr`。不設這個，per-IP rate limit 形同虛設。

## log 遮蔽敏感資料

log 會不小心把密碼、token 印出來。gortex 的 logger 用 `BodyRedactor` 把請求 body 裡的敏感欄位遮掉（預設 `DefaultBodyRedactor`），header / query 也比照處理。

這裡有個要直說的限制：**`LogResponseBody` 是 no-op**，原始碼註解明寫 deprecated、不生效，框架不支援記錄回應 body。4xx / 5xx 則是用〈[零分配的秘密：Context pool 與 smartParams](/posts/gortex-zero-allocation-context/)〉那個 tracked responseWriter 拿到的**真實狀態碼**，分別記在 Warn / Error。會記什麼、不會記什麼，講清楚比假裝全能好。


## 補充筆記

- Binder 用 `http.MaxBytesReader` 把 JSON body 卡在 1 MiB（multipart 另有上限），擋 oversize DoS。
- CORS 可設允許來源（預設 `*`，上線應收緊）；CSRF 用 double-submit token（預設 32 bytes、SameSite Lax），缺 / 錯 → 403。
- JWT：secret ≥ 32 bytes、HS256-only（擋 algorithm confusion）、`typ` 分 access / refresh，舊的無 typ token 一律拒。
- rate limit 發 `X-RateLimit-*` / `Retry-After`；但按 IP 做 rate limit 要配 `TrustedProxies`，否則被偽造 `X-Forwarded-For` 繞過。
- log 遮蔽請求 body 敏感欄位；`LogResponseBody` 是 no-op，4xx / 5xx 用 tracked writer 的真實狀態記 Warn / Error。

原始碼：[yshengliao/gortex](https://github.com/yshengliao/gortex)。

---

> 想法與技術判斷出自 Sheng，和 Claude 一起起草 · 環境 Go 1.25（gortex go.mod）· 本系列為事後回填整理 · 列入 20260613 blog 翻新計劃，新漆未乾。