go 的 net/http 默认不防头部注入,必须手动过滤用户输入的 header 值和名:先 trimspace,再用 strings.containsany 检查 \r\n\x00;header 名需白名单校验;fasthttp 等第三方库行为不同,须统一过滤并独立校验每层。

Go 的 net/http 默认不防头部注入,必须手动过滤
Go 标准库的 http.ResponseWriter 和 http.Header 本身不做任何 HTTP 头部字段合法性校验。如果你把用户输入直接拼进 SetHeader、WriteHeader 或 Header().Set,就可能触发 CRLF 注入(即注入 \r\n 换行符),导致响应拆分、缓存污染甚至 XSS。
常见错误现象:http: invalid header field name 这类错误只在字段名含非法字符时触发,但字段值里的 \r\n 完全不会被拦截 —— 它会原样写入响应流。
- 所有用户可控的 header 值(如
X-Forwarded-For、User-Agent、自定义X-Trace-ID)都必须过滤 - 不要依赖中间件或框架自动处理;标准
net/http没有这层防护 - 过滤时机必须在调用
w.Header().Set()或w.Header().Add()之前
用 strings.TrimSpace + strings.ContainsAny 快速清理 header 值
HTTP/1.1 规范要求 header 值不能含控制字符(尤其是 \r、\n、\0),也不应以空格或制表符开头/结尾。最轻量且有效的做法是:先去首尾空白,再拒绝含 \r、\n、\0 的字符串。
示例:
立即学习“go语言免费学习笔记(深入)”;
// userValue 来自 query / form / header / JSON
cleanValue := strings.TrimSpace(userValue)
if strings.ContainsAny(cleanValue, "\r\n\x00") {
cleanValue = "" // 或替换为默认值,如 "unknown"
}
w.Header().Set("X-User-Tag", cleanValue)
- 不用正则;
strings.ContainsAny是 O(n),足够快且无逃逸 - 别只删
\r\n而忽略\x00—— 某些底层协议栈(如 fasthttp)对 null 字节更敏感 - 不要用
url.PathEscape或html.EscapeString处理 header 值 —— 它们针对不同上下文,会破坏原始语义(比如把空格转成%20就不是合法 header 值)
自定义 header 名也要校验,避免非法字段名绕过
虽然 w.Header().Set("X-User-\r\nX-Injected", "1") 会触发 panic(因为字段名含 \r\n),但攻击者可能构造看似合法、实则带编码或 Unicode 空格的字段名(如 "X-User-\u200b"),某些代理或客户端解析异常时仍可能出问题。
- header 名必须满足 RFC 7230:只含
A-Z、a-z、0-9、-,且不能以-开头或结尾 - 建议白名单校验:用
regexp.MustCompile(`^[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9]$`)匹配,比黑盒过滤更可靠 - 如果字段名也来自用户(少见但存在),必须在
Header().Set前校验,否则 panic 会暴露服务细节
第三方库如 fasthttp 的行为差异要特别注意
fasthttp 不用标准 net/http 的 header map,它内部用字节切片拼接响应,对非法字符容忍度更低 —— 某些版本遇到 \r\n 会直接 panic,有些则静默截断。这意味着同一段过滤逻辑,在 net/http 下“看起来正常”,在 fasthttp 下可能 crash 或行为不一致。
- 统一用
strings.ContainsAny(val, "\r\n\x00")判断,别依赖库的 panic 行为做防御 - 若用
fasthttp.RequestCtx.SetContentType类方法,传入的 content-type 值同样需过滤(它不校验值中的换行) - 测试时务必覆盖两种 server 实现,尤其关注 4xx 请求体中携带恶意 header 的 case
真正麻烦的不是过滤逻辑本身,而是 header 值可能经过多层透传(反向代理、CDN、网关),每一层都可能重新注入或解码。只要有一处没过滤,防线就垮了。所以别信“上游已经处理了”,每个接收点都要独立校验。










