go的net/http不拦截http头非法字符,必须对所有用户可控header值用trimspace+containsany清洗,header名须用白名单正则校验,且清洗需贯穿请求生命周期每个触点。

Go 的 net/http 对请求头和响应头里的非法字符完全不拦截,r.Header 和 w.Header().Set() 都是“照单全收”。不手动过滤,CRLF 注入、日志污染、响应头分裂就不是理论风险,而是必然发生。
用 TrimSpace + ContainsAny 做 header 值清洗最可靠
所有用户可控的 header 值(包括 X-Forwarded-For、User-Agent、自定义 X-Trace-ID)必须在写入或透传前清洗。RFC 7230 明确禁止 \r、\n、\x00 出现在 header 值中,且首尾空格可能被代理截断或忽略。
-
strings.TrimSpace()必须先做——去掉开头/结尾空格和制表符,避免某些 CDN 或反向代理误判 - 紧接着用
strings.ContainsAny(cleanValue, "\r\n\x00")判断是否含非法字符;命中就拒绝或 fallback(比如设为"unknown") - 别用正则匹配——开销大、易漏、还可能逃逸;也别只删
\r\n而忽略\x00,fasthttp等底层库对 null 字节更敏感 - 不要用
url.PathEscape()或html.EscapeString()处理 header 值——它们改变语义,比如把空格转成%20后就不再是合法 header 值
header 名必须走白名单正则校验
w.Header().Set("X-User-\r\nInjected", "1") 会 panic 报 http: invalid header field name,但这只是字段名含 CRLF 的兜底行为,防不住 Unicode 零宽空格(\u200b)、全角短横、开头/结尾连字符等绕过手段。
- RFC 7230 规定 header 名只能含
A-Z、a-z、0-9、-,且不能以-开头或结尾 - 推荐固定正则:
^[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9]$,用regexp.MustCompile编译一次复用 - 不要用
strings.ToLower()归一化后再匹配——老旧代理对大小写敏感,规范本身允许混合大小写 - 客户端侧同样要校验:如果你用
http.Client代理请求,上游传来的 header 名(如X-Real-IP)也要走同一套白名单
中间件里修改 header 前必须重新清洗
很多框架(Gin、Echo)中间件会在 Next() 前改 r.Header,比如加 X-Request-ID 或重写 Host。但若你在这个阶段对 Accept、Content-Type 等标准头做了粗暴操作(例如 strings.ReplaceAll(v, "\n", "")),可能导致下游解析失败或 MIME 类型误判。
- 不要直接
r.Header.Set("Accept", ...)——Accept是逗号分隔的 MIME 类型列表,删换行可能破坏结构 - 如果必须修改,先用
r.Header.Values("Accept")拿到全部值,逐个清洗后再r.Header.Set() - 注意
r.Header是map[string][]string,.Get()只取第一个值,但 RFC 允许多值,透传时得遍历处理 - 尤其警惕日志中间件:
log.Printf("User-Agent: %s", r.Header.Get("User-Agent"))若没清洗,控制字符会让日志被截断或伪造换行
ReverseProxy 场景下 upstream header 必须重置
用 httputil.ReverseProxy 代理 S3 或其他外部服务时,上游返回的 X-Amz-Request-ID、Server、X-Amz-Version-Id 等头默认原样透传,Delete() 单个字段不可靠——因为 ReverseProxy 在 copyHeader() 时已把整个 resp.Header 复制进响应,删了也可能被后续逻辑重加。
- 正确做法是自定义
Transport,在RoundTrip()返回后丢弃原始resp.Header,替换为全新http.Header{}或预设白名单头 - 清洗不能只做一次:上游响应头里的值(如
X-Forwarded-For)可能带注入字符,必须在写入新响应前再过一遍TrimSpace + ContainsAny - 别依赖
Director函数——它只改请求头,不碰响应头;真正起效的是ModifyResponse或自定义Transport
header 过滤不是“加个中间件就完事”的事,它必须贯穿请求生命周期每个触点:从 http.Request 解析开始,到 http.ResponseWriter 写出结束,中间每层透传、改写、日志、代理都得独立清洗。最容易被忽略的是客户端侧过滤和 Values() 遍历——以为 .Get() 就够了,结果多值 header 里藏着一个带 \r\n 的恶意值。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











