Files
go_blog/handlers/api.go
T
kevin 2e3888d5b1 fix: 完成 P0 安全修复 #26——请求体大小限制防未认证内存耗尽 DoS
- 新增 middleware/bodylimit.go:BodyLimit 中间件,非 multipart 统一
  4 MiB(覆盖文章正文上限);multipart 按平台上传策略派生(启用类型
  限制与全局默认取最大 +1 MiB 开销,下限不低于编译期默认)。
  Content-Length 已知且超限读体前直接 413,其余经 MaxBytesReader 截断
- handlers/api.go:bindJSON 识别 *http.MaxBytesError → 413 +
  request_too_large(i18n 中英新增)
- main.go:中间件顺序调整为 SetUserContext → BodyLimit → CSRF——
  必须先于 CSRF(其解析 multipart 会读取整个请求体),SetUserContext
  提前使 413 文案可按请求语言翻译;测试环境链同步(security_test.go)
- middleware/auth.go:apiAuthError 更名 apiError(BodyLimit 复用)
- 新增 handlers/bodylimit_test.go:超限 JSON 两种形态(已知长度/
  chunked)413、正常体放行至认证层、3MB multipart 拒绝且附件表零写入、
  GET 不受影响
- SECURITY_TODO.md:#26 勾选完成并记录验证;API 化复审新增 #26–#32
  待办清单(P1 注册/评论限流、P2 favicon 魔数校验/最后管理员竞态/
  置顶权限、P3 零碎项)与执行顺序

go build / vet / test -race ./... 全绿
2026-08-27 21:23:19 +08:00

78 lines
2.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// APIOK 返回统一成功响应。
// redirect 非空时附带相对跳转地址(原 302 目标,可含 ?saved=1 等 query);
// data 额外字段会合并进响应。
func APIOK(c *gin.Context, redirect string, data gin.H) {
resp := gin.H{"ok": true}
if redirect != "" {
resp["redirect"] = redirect
}
for k, v := range data {
resp[k] = v
}
c.JSON(http.StatusOK, resp)
}
// APIError 返回统一错误响应:{ok:false, code, error}。
// trKey 是 i18n 键,直接作为 code 返回;error 为该键按请求语言的翻译文案。
// trKey 为空时回退到通用键 "api_error"。
func APIError(c *gin.Context, status int, trKey string) {
tr := getTr(c)
code := trKey
if code == "" {
code = "api_error"
}
msg := tr[code]
if msg == "" {
msg = tr["api_error"]
}
c.JSON(status, gin.H{
"ok": false,
"code": code,
"error": msg,
})
}
// APIErrorf 同 APIError,但 i18n 文案可按 fmt.Sprintf 格式化
// (针对含 %d/%s 占位符的键,如 comments_too_long)。
func APIErrorf(c *gin.Context, status int, trKey string, args ...interface{}) {
tr := getTr(c)
msg := tr[trKey]
if msg == "" {
msg = tr["api_error"]
} else if len(args) > 0 {
msg = fmt.Sprintf(msg, args...)
}
c.JSON(status, gin.H{
"ok": false,
"code": trKey,
"error": msg,
})
}
// bindJSON 将 JSON 请求体绑定到 v。
// 绑定失败时返回 400 + api_invalid_request,并返回 false
// 请求体超出 BodyLimit 中间件设置的上限时返回 413 + request_too_large
// SECURITY_TODO #26)。使用前必须保证请求是 JSON。
func bindJSON(c *gin.Context, v interface{}) bool {
if err := c.ShouldBindJSON(v); err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
APIError(c, http.StatusRequestEntityTooLarge, "request_too_large")
return false
}
APIError(c, http.StatusBadRequest, "api_invalid_request")
return false
}
return true
}