fix(security): 修复 P1 高危项(OAuth2 state / 伪造客户端IP / CRLF 邮件头注入)

- OAuth2: state 改为 crypto/rand 随机值并写入独立短期 cookie
  (主会话为 SameSite=Strict,跨站回调不携带,不能放主会话);
  回调用 ConstantTimeCompare 校验 state,缺失/不匹配返回 403,
  校验后立即清除保证一次性使用。原硬编码 mailgo_oauth2_state
  可被利用做授权码注入/登录 CSRF。
- 代理信任: engine.SetTrustedProxies 仅信任 127.0.0.1/::1。
  外部直连时 X-Forwarded-For 完全不可信,防止伪造客户端 IP
  绕过登录封禁或恶意封禁他人;本机 Caddy/Nginx 转发不受影响。
- CRLF 注入: Web 写信的 To/Cc/Subject 及附件文件名不再原样拼入
  MIME 头。新增 sanitizeHeaderField(strip CR/LF/NUL)、
  subject 按 RFC 2047 编码、附件名用 mime.FormatMediaType
  (RFC 2231);附件下载的 Content-Disposition 同步修复。
  消息构建抽为 buildOutgoingMessage 纯函数便于测试。
- test: 新增 13 个回归测试(trustedproxy / mail_injection /
  oauth2_state),覆盖伪造 XFF、注入载荷、state 校验全部分支。
This commit is contained in:
2026-08-19 16:31:25 +08:00
parent 551ed981de
commit c725d0b91e
8 changed files with 559 additions and 82 deletions
+8
View File
@@ -177,6 +177,14 @@ func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storag
engine.Use(gin.Logger())
engine.Use(gin.Recovery())
// 仅信任本机回环上的反向代理(Caddy/Nginx)。外部直连时
// X-Forwarded-For 不可信,防止伪造客户端 IP 绕过登录封禁或
// 恶意封禁他人 IP。gin 对 Unix socket 监听无条件信任转发头,
// 因此 socket 必须保持仅本机可达。
if err := engine.SetTrustedProxies([]string{"127.0.0.1", "::1"}); err != nil {
return nil, fmt.Errorf("设置可信代理失败: %w", err)
}
// Session store (cookie-based). The signing key comes from the config
// file (auto-generated random key) or the MAILGO_SECRET_KEY env var.
cookieStore := cookie.NewStore([]byte(cfg.SecretKey))