header名必须用正则白名单校验,不能只靠panic拦截;值需trimspace+containsany检查防crlf和null字节;可信header来源须绑定代理链并精确配置settrustedproxies。

Header 名必须用正则白名单校验,不能只靠 panic 拦截
Gin 或 net/http 的 Header.Set() 在遇到非法字段名时会 panic,比如 http: invalid header field name,但这只是兜底机制,无法防御 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()归一化后再匹配——老旧代理对大小写敏感,规范本身允许混合大小写 - 别依赖框架自动过滤——Gin 不做 header 名合法性检查,
c.Request.Header.Get()只读取,不校验 - 所有动态构造的 header 名(如拼接
"X-User-" + userID)都必须走这道校验
Header 值必须 TrimSpace + ContainsAny 检查,防 CRLF 和 null 字节注入
net/http 对 header 值完全不做清洗,\r、\n、\x00 会被原样写入响应流,触发 CRLF 注入或 fasthttp 等底层库崩溃。这不是“少见风险”,而是只要透传未清洗的 X-Forwarded-For 或 User-Agent 就可能中招。
清洗逻辑必须在调用 w.Header().Set() 或 c.Header() 前执行:
cleanValue := strings.TrimSpace(rawValue)
if strings.ContainsAny(cleanValue, "\r\n\x00") {
cleanValue = "unknown" // 或直接拒绝请求
}
c.Header("X-Forwarded-For", cleanValue)
- 必须先
TrimSpace——某些代理会截断开头空格,导致后续校验失效 - 必须同时检查
\r、\n、\x00——漏掉\x00在 fasthttp 或某些 HTTP/2 实现里更危险 - 客户端侧也要清洗:如果你用
http.Client代理请求,上游 header 值(如X-Real-IP)同样要过这关,不能只服务端做
白名单校验不能用正则模糊匹配,必须精确枚举合法值
用 regexp.MustCompile(`^[a-zA-Z0-9_]+$`) 匹配参数值是典型陷阱。攻击者可用零宽空格(\u200b)、SQL 注释符(/**/)、Unicode 变体绕过,而你的正则根本识别不了。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
真正安全的做法只有一种:预定义合法集合,运行时精确比对:
validSortFields := map[string]bool{"name": true, "email": true, "created_at": true}
if !validSortFields[field] {
return errors.New("invalid sort field")
}
- 别用
net.ParseIP()返回非 nil 就放行——127.0.0.1\u200b能过解析,但后续net.IPNet.Contains()会失败 - 动态拼接 SQL 标识符(如表名、ORDER BY 字段)必须分层白名单:字段、方向、表名各自独立校验,不能合并
- 响应字段过滤别碰
json:"-"标签——用专用 struct 显式赋值,避免反射误放行
可信 Header 来源必须绑定代理链,不能无条件信任 X-Forwarded-For
X-Forwarded-For 和 X-Real-IP 可被客户端任意伪造,比如发个 X-Forwarded-For: 1.2.3.4, 127.0.0.1 就能绕过简单字符串比对。Gin 的 c.ClientIP() 默认只信任 127.0.0.1,线上经 Nginx/CDN 后返回的全是内网地址(如 10.0.0.5),根本不是真实出口 IP。
关键不是“头字段内容”,而是“谁加的这个头”:
- 必须显式调用
engine.SetTrustedProxies([]string{"100.64.0.0/10", "203.208.60.0/24"}),网段要精确(阿里云 SLB 是/10,腾讯云 CLB 是/11) - 取
X-Forwarded-For时,必须按逗号分割后从左往右找第一个ip.IsGlobalUnicast()为 true 的 IP,跳过所有私有地址(127.0.0.0/8、10.0.0.0/8等) - 永远用
net.ParseIP()解析后再判断,别直接字符串比较——IPv6 地址带方括号([2001:db8::1]),需先去掉
最易被忽略的是:白名单规则和代理链解析必须耦合在同一中间件里,分开写就等于把校验逻辑暴露给伪造头。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










