Files
go_blog/middleware/bodylimit.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

86 lines
2.9 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 middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"go_blog/models"
)
// jsonBodyLimit 是非 multipart 请求体(JSON / 表单 / 未知类型)的统一上限
// SECURITY_TODO #26)。4 MiB 覆盖最大的合法载荷——文章 Markdown 正文;
// 登录/注册/评论等小载荷共用同一上限,避免按端点维护多套配置。
const jsonBodyLimit int64 = 4 << 20
// multipartOverhead 叠加在上传策略派生的大小之上,容纳 multipart 编码
// 开销(边界、_csrf/session_token 等表单字段)。
const multipartOverhead int64 = 1 << 20
// BodyLimit 限制不安全方法(POST/PUT/PATCH/DELETE)的请求体大小,
// 防止未认证的内存/磁盘耗尽 DoSSECURITY_TODO #26):
//
// - Content-Length 已知且超限时立即返回 413,不读取请求体;
// - 其余请求体经 http.MaxBytesReader 封装:超限后读取立即失败,
// JSON 路径由 handlers.bindJSON 识别并转为 413multipart 路径的
// 解析在 CSRF/处理器中进行,同样在限额处截断。
//
// 必须注册在 CSRFProtect 之前——CSRF 中间件解析 multipart 表单
// (查找 _csrf 字段)会读取整个请求体;必须在 SetUserContext 之后,
// 使 413 文案可按请求语言翻译(apiError 读取上下文中的 tr)。
func BodyLimit() gin.HandlerFunc {
return func(c *gin.Context) {
switch c.Request.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
c.Next()
return
}
if c.Request.Body == nil {
c.Next()
return
}
limit := bodyLimitFor(c.Request.Header.Get("Content-Type"))
if c.Request.ContentLength > limit {
apiError(c, http.StatusRequestEntityTooLarge, "request_too_large")
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit)
c.Next()
}
}
// bodyLimitFor 依据 Content-Type 返回请求体上限:
// multipart 按平台上传策略派生,其余(JSON / urlencoded / 未知)统一
// 走 JSON 上限。urlencoded 本身已被 net/http 限制在 1 MiB 内
// parsePostForm),这里再统一封顶一层。
func bodyLimitFor(contentType string) int64 {
ct := strings.ToLower(strings.TrimSpace(contentType))
if strings.HasPrefix(ct, "multipart/form-data") {
return maxUploadBodyLimit() + multipartOverhead
}
return jsonBodyLimit
}
// maxUploadBodyLimit 返回当前平台策略下可被接受的最大单文件大小:
// 全局默认值与各启用类型的按类型限制取最大值,且不低于编译期默认,
// 防止异常的零值配置把上限压垮到小于合法上传。
func maxUploadBodyLimit() int64 {
cfg := models.GetUploadConfig()
max := cfg.DefaultMaxSize
if max < models.DefaultUploadMaxSize {
max = models.DefaultUploadMaxSize
}
types := models.GetUploadFileTypes()
for i := range types {
if !types[i].Enabled {
continue
}
if s := types[i].EffectiveMaxSize(cfg.DefaultMaxSize); s > max {
max = s
}
}
return max
}