go标准库不校验http头值中的\r\n\x00,直接写入会导致crlf注入;需在set()/add()前用strings.trimspace和strings.containsany校验并过滤控制字符。

Go 标准库不拦截 header 值里的 \r\n
net/http 的 Header().Set() 和 Header().Add() 完全不会检查用户输入是否含 \r、\n 或 \x00。只要值里有这些字符,就可能触发 CRLF 注入——响应被拆分成多段,攻击者能伪造任意响应头甚至插入响应体。
常见错误现象:http: invalid header field name 这类错误只在 header 名非法时抛出,header 值里的 \r\n 会被静默写入 HTTP 流,浏览器或代理会按实际字节解析,毫无预警。
- 所有来自请求的字段都算“用户可控”:比如
r.Header.Get("X-Forwarded-For")、r.FormValue("trace_id")、JSON body 解析出的custom_header字段 - 不要依赖中间件统一过滤——不同 handler 可能绕过中间件直接调用
w.Header().Set() - 过滤必须在调用
Set()或Add()之前做,晚一步就失效
用 strings.TrimSpace + strings.ContainsAny 做轻量校验
HTTP/1.1 规范明确禁止 header 值含控制字符,且不应首尾空白。最直接有效的做法是先去空格,再拒绝含危险字符的字符串。
示例代码:
cleanValue := strings.TrimSpace(userValue)
if strings.ContainsAny(cleanValue, "\r\n\x00") {
cleanValue = "unknown" // 或返回 400,取决于业务
}
w.Header().Set("X-Request-ID", cleanValue)
-
strings.ContainsAny是 O(n) 时间复杂度,无内存逃逸,比正则快得多也更安全 - 必须同时检查
\r、\n、\x00—— 某些底层库(如fasthttp)对 null 字节更敏感,仅删换行符不够 - 别用
url.PathEscape或html.EscapeString处理 header 值:前者把空格转成%20,后者把转成 <code>,都不符合 header 值语法
header 名也要白名单校验,不能只防值
字段名不像字段值那样允许任意 Unicode,它受严格语法限制:只能含 token 字符(字母、数字、!#$%&'*+-.^_`|~),且不能含空格或控制符。但更关键的是——你根本不该让用户决定 header 名。
- 硬编码 header 名(如
"X-User-Tag"、"Content-Type")是最安全的 - 如果业务真需要动态字段名(极少见),必须用白名单匹配,例如:
validHeaders := map[string]bool{"X-Trace-ID": true, "X-Correlation-ID": true} - 绝对不要拼接用户输入作为字段名:
w.Header().Set(userSuppliedName, value)是高危操作
第三方框架(Gin/Echo)和 fasthttp 的行为差异
Gin 和 Echo 默认不自动过滤 header 值,它们底层仍走 net/http,所以同样存在注入风险。而 fasthttp 更激进:它对 \x00 会直接 panic,但对 \r\n 仍放行——这意味着你在标准库里没暴露的问题,在 fasthttp 里可能变成崩溃,而不是漏洞利用。
- 不要假设框架“替你做了防护”,所有 header 写入点都要独立校验
- 若项目混用
net/http和fasthttp(比如反向代理层),需确保过滤逻辑一致,尤其对\x00的处理策略要统一 - header 注入不是“有没有”的问题,而是“在哪一层漏掉”的问题——每处
Set()都是独立风险点
w.Header().Set() 前都要插一段校验。它不难,但容易漏。尤其是带条件分支的 handler,或者复用已有工具函数时,很容易把过滤逻辑丢在某个 if 分支外面。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











