Go 的 net/http 无法可靠防御 Slow POST 攻击,因模型不匹配;唯一有效应用层手段是 http.MaxBytesReader,配合显式配置 ReadTimeout、WriteTimeout 和 IdleTimeout,并避免 IP 计数等无效防护。
go 的 net/http 服务本身无法可靠防御 slow post 攻击——不是配置不到位,而是模型不匹配。 它在连接建立后才启动 handler,而 slow post 正是卡在「连接已建、body 未读完」这个窗口期耗尽资源。硬加限流或计数器,只会让问题更隐蔽、更难排查。
为什么 http.MaxBytesReader 是唯一真正有效的应用层手段
Slow POST 的核心是:攻击者声明一个巨大的 Content-Length(比如 1GB),然后每几秒只发 1 字节,迫使服务器长期维持连接并分配缓冲区。http.MaxBytesReader 是 Go 标准库中唯一能在这个阶段主动干预的机制:
- 它包装
r.Body,在每次Read()调用时检查累计读取量,超限立即返回io.ErrUnexpectedEOF - 与
http.Server底层协同,不会缓存后续恶意字节;而io.LimitReader只拦应用层读取,底层 TCP 还在收包 - 自动触发 413 响应,并关闭连接——前提是必须在任何读取操作前设置
- 对分块传输(
Transfer-Encoding: chunked)同样生效,因为此时r.ContentLength == -1,不能靠它做判断
http.Server 的超时参数必须显式设,否则毫无意义
默认情况下,Go 的 http.Server 几乎没有超时控制。攻击者可以无限期地维持一个半开连接,只要不发完 body,就不会触发任何 timeout。关键三项必须全部显式配置:
-
ReadTimeout:从 accept 连接到完成请求头 + 请求体读取的总时间上限(注意:它包含 slow header 和 slow body 阶段) -
WriteTimeout:从开始写响应到写完的总时间,防 slow read 类攻击拖住响应流 -
IdleTimeout:连接空闲(无数据收发)的最大持续时间,防 slow read 中的 zero-window 持久占位
例如:ReadTimeout: 5 * time.Second 意味着:哪怕你只发了 GET / HTTP/1.1\r\nHost: 就停住,5 秒后连接也会被强制关闭——这对 slow headers 同样有效。
别在 handler 里做 IP 计数或动态封禁
所有基于 sync.Map 或本地 map 维护 per-IP 请求计数的方案,在高并发 Slow POST 场景下都会迅速成为瓶颈:
- 每个慢连接都占用一个 goroutine,且长时间存活;goroutine 数暴涨直接压垮调度器
- 多个 goroutine 同时更新同一 IP 的计数器,
sync.Map的伪原子性在极端争抢下仍会退化为锁竞争 - Redis 等外部存储引入网络延迟和单点故障,反而放大误判率(如正常用户因网络抖动被连带封禁)
- 攻击者只需轮换源端口、伪造
X-Forwarded-For或走代理,就能绕过绝大多数 IP 维度的逻辑
真正该做的三件事:守边界、保可用、留线索
Go 应用层不该试图“识别并拦截攻击”,而应确保自身在压力下不崩溃、不误伤、不丢日志:
- 用
http.MaxBytesReader+ 显式ReadTimeout控制单连接资源消耗——这是你能守住的最内层防线 - 对外部依赖(DB、下游 HTTP)强制加
context.WithTimeout和熔断(如gobreaker),避免一个慢 POST 触发级联雪崩 - 记录原始连接元信息(真实客户端 IP、TLS 握手耗时、首字节到达时间、body 开始读取时间),这些才是给 Nginx/CDN 做联动封禁的真实依据
复杂点在于:ReadTimeout 设太短会误杀弱网用户,设太长又起不到防护作用;而 MaxBytesReader 的阈值必须结合业务最大合法上传尺寸来定——这两个数字没有通用解,只能靠真实流量采样+压测校准。











