必须手动赋值r.Body = http.MaxBytesReader(w, r.Body, limit)才生效,否则json.NewDecoder等会无限制读取导致OOM;需在defer r.Body.Close()前执行,且参数顺序为(w, r.Body, n),不可颠倒。
http.MaxBytesReader必须手动赋值给r.Body才生效
不设这道闸门,json.newdecoder(r.body) 或 xml.unmarshal 会直接读完整个请求体——攻击者发个 2gb 的垃圾 json,进程在解析前就 oom 了。现象是 panic: runtime out of memory 或 rss 持续飙升后 500。
关键点:
-
http.MaxBytesReader(w, r.Body, 5*1024*1024)返回的是新io.ReadCloser,必须显式赋值:r.Body = http.MaxBytesReader(...) - 顺序不能错:得在
defer r.Body.Close()之前执行,否则可能 panic - 参数第一个是
w http.ResponseWriter,不是r *http.Request;写成http.MaxBytesReader(r, ...)会编译失败 - 它只限制 body 字节数,和
http.Server{MaxHeaderBytes: 8192}是两回事,header 和 body 要分开防
流式解包时别信协议头里的长度字段
伪造 Content-Length: 2147483647 或恶意 Transfer-Encoding: chunked 头,能让服务尝试分配超大 buffer。Go 的 http.Request.Body 是裸 io.ReadCloser,不做任何校验。
安全做法:
- 对自定义二进制协议(如
[4]byte len + []byte payload):先用io.ReadFull(conn, headerBuf)读头,检查长度是否合理(比如),再用 <code>io.LimitReader(conn, int64(len))包一层传给后续解析 - 绝不用
make([]byte, maliciousLen)—— 攻击者发0xffffffff当长度,make就直接 panic - JSON Lines 或 protobuf 流:用
json.Decoder或proto.UnmarshalStream,它们基于io.Reader,按需读、边解边丢,内存稳定在 KB 级
multipart 表单上传必须跳过 ParseMultipartForm
r.ParseMultipartForm(32 看似设了 32MB 限制,但只是内存缓冲上限;攻击者可发 100 个 31MB 的 part,照样耗尽 fd 和磁盘空间。更糟的是,它内部会把整个请求体读完才开始拆分。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
正确姿势:
- 跳过
r.ParseMultipartForm,改用multipart.NewReader(r.Body, boundary)手动逐 part 解析 - 每个
part.Header里的Content-Length可伪造,真正安全的是用io.CopyN(dst, part, expectedSize)控制单 part 最大读取量 - 务必调用
part.Close()和mr.Close(),否则 fd 泄露 - 如果用了
io.Copy写入磁盘或对象存储,记得用io.CopyBuffer(dst, src, make([]byte, 1 把缓冲设到 1MB,尤其在 NFS 上能提速 3–5 倍
zlib 或其他压缩流里读定长字段要加 bufio.Reader
zlib.Reader.Read 返回字节数不可控,可能把一个 uint64 字段切成两段,导致解析逻辑必须维护状态机,极易出错。
可靠方案:
- 用
bufio.NewReaderSize(zlibReader, 64*1024)包一层,缓冲区大小 ≥ 单次最大解析单元(如最大消息头 + 预留字段) - 读定长结构必须用
io.ReadFull(r, buf[:]),它会自动重试直到填满,彻底规避跨 chunk 拆分 - 不要用原始
zlib.Reader直接对接二进制协议解析器,除非你愿意为每个字段写状态恢复逻辑

















