必须对HTTP header值做TrimSpace+ContainsAny检查以防止CRLF注入,header名需用正则白名单校验,客户端和服务端均需过滤,且不可混用URL/HTML转义。
直接过滤 header 值:必须做 TrimSpace + ContainsAny 检查
go 标准库 net/http 对 header 值完全不做合法性校验,\r、\n、\x00 会原样写入响应流,触发 crlf 注入。不能依赖框架或中间件自动处理,必须在调用 w.header().set() 或 w.header().add() 前完成清洗。
推荐做法是两步组合:
- 先用
strings.TrimSpace()去首尾空白(避免开头空格被某些代理截断) - 再用
strings.ContainsAny(cleanValue, "\r\n\x00")判断是否含非法字符;命中则拒绝或 fallback 到默认值(如"unknown")
别用正则——开销大且易漏;也别只删 \r\n 而忽略 \x00,fasthttp 等底层库对 null 字节更敏感。
header 名必须白名单校验,不能只靠 panic 拦截
虽然 w.Header().Set("X-User-\r\nX-Injected", "1") 会 panic 报 http: invalid header field name,但这只是字段名含 \r\n 的兜底行为,无法防御 Unicode 零宽空格(\u200b)、全角短横等绕过手段。
RFC 7230 明确要求 header 名只能含 A-Z、a-z、0-9、-,且不能以 - 开头或结尾。建议用固定正则白名单校验:
var headerNameRE = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9]$`)
if !headerNameRE.MatchString(userHeaderName) {
return errors.New("invalid header name")
}
注意:不要用 strings.ToLower() 或大小写归一化后再匹配——某些老旧代理对大小写敏感,且规范本身允许大小写混合。
客户端发请求时也要过滤 header 值,不是服务端专属
如果你用 http.Client 代理用户请求(比如网关、反向代理),那么从上游收到的 header 值(如 X-Forwarded-For、User-Agent)同样可能带注入字符,不能直接透传给下游服务。
典型场景包括:
- 用
req.Header.Get("X-Real-IP")取值后,再设到新请求的newReq.Header.Set("X-Real-IP", ip)前必须清洗 - 通过
http.RoundTripper实现请求拦截时,在RoundTrip()中修改 header 前,所有用户来源字段都要走一遍TrimSpace + ContainsAny
漏掉客户端侧过滤,等于把注入风险转嫁给后端服务——尤其当后端不是 Go 时,行为更不可控。
别混淆 header 值过滤和 URL/HTML 上下文转义
有人误用 url.PathEscape() 或 html.EscapeString() 处理 header 值,这是错的。HTTP header 值不允许 URL 编码(%20 不是合法空格),也不应 HTML 转义( 会被当成字面量而非标签)。
header 值的合法字符集就是 ASCII 可见字符 + 空格 + 制表符(但首尾空格/制表符需 trim),其他控制字符一律禁止。清洗目标是“让字符串能安全出现在 HTTP 响应头行中”,不是“让它能在 HTML 页面里显示”。
最常被忽略的是:日志打印 header 值时如果没过滤,攻击者可通过构造恶意 header 触发日志注入(如伪造换行塞进 log 文件),所以清洗动作最好统一抽成一个函数,在所有入口点复用。











