- 新增 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 ./... 全绿
86 lines
2.9 KiB
Go
86 lines
2.9 KiB
Go
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)的请求体大小,
|
||
// 防止未认证的内存/磁盘耗尽 DoS(SECURITY_TODO #26):
|
||
//
|
||
// - Content-Length 已知且超限时立即返回 413,不读取请求体;
|
||
// - 其余请求体经 http.MaxBytesReader 封装:超限后读取立即失败,
|
||
// JSON 路径由 handlers.bindJSON 识别并转为 413;multipart 路径的
|
||
// 解析在 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
|
||
}
|