白名单校验必须采用精确枚举而非正则过滤,预定义合法值集合并做字符串精确比对;动态拼接SQL标识符需分层校验字段、方向、表名;IP白名单热更新须用atomic.Value封装不可变结构;响应字段过滤应使用专用响应struct显式赋值。

白名单校验必须是精确枚举,不是正则过滤
任何试图用regexp.MustCompile(`^[a-zA-Z0-9_]+$`)匹配参数值的做法都不可靠。攻击者能用零宽空格(\u200b)、SQL注释符(/**/)、Unicode变体绕过,而你的正则根本识别不了。
真正安全的写法只有一种:预定义合法值集合,运行时做字符串精确比对。
- ✅ 正确:定义
validSortFields := map[string]bool{"name": true, "email": true, "created_at": true},然后if !validSortFields[field] { return errors.New("invalid field") } - ❌ 危险:用
strings.TrimSpace+strings.ReplaceAll(s, "'", "''")清理后再判断——score/**/仍能通过 - ❌ 更危险:依赖
net.ParseIP返回非nil就认为IP合法——127.0.0.1\u200b也能过解析,但后续net.IPNet.Contains会失败
动态拼接SQL标识符时,白名单要覆盖字段+方向+表名三层
ORDER BY、GROUP BY、LIMIT、OFFSET、表名、字段名这些都不是值,Go驱动不支持占位符,必须拼接。但拼接前必须分层校验,漏一层就等于开后门。
- 排序字段和方向要分开白名单:
validFields["score"]为true,不代表validDirs["DESC"]也自动可信;"score ASC; DROP TABLE users"里ASC;本身就能触发语法错误或多语句执行 - 表名不能靠正则或小写转换放行,得硬编码映射:
switch input { case "user": return "users_prod"; case "order": return "orders_archive" } - 别查
INFORMATION_SCHEMA.TABLES动态取表名——这本身就要执行SQL,且结果可能被缓存或污染,反而增加攻击面
IP白名单热更新必须用atomic.Value+不可变结构
直接替换全局map[string]bool会导致并发读写崩溃,panic: concurrent map read and map write。热更新不是“换map”,而是“换指针快照”。
立即学习“go语言免费学习笔记(深入)”;
- 把白名单封装成结构体:
type Whitelist struct { ips []*net.IPNet; domains []string },所有字段只读 - 用
var wl atomic.Value存*Whitelist指针,更新时wl.Store(newWL),读取时wl.Load().(*Whitelist) - 预解析CIDR:
net.ParseCIDR("192.168.0.0/16")在加载阶段完成,运行时只调ipnet.Contains(clientIP),避免每次请求都解析字符串
响应字段过滤别碰json标签,用专用struct显式赋值
json:"-"或json:",omitempty"不是安全机制,只是序列化开关。字段存在、值非零,就会输出——哪怕你写了json:"-",反射或调试日志仍可能暴露。
- ✅ 稳定做法:为每个接口定义独立响应struct,如
UserDetailResponse,只含ID/Name/Email三个字段,handler里手动赋值 - ❌ 危险路径:把数据库模型
User直接传给json.Marshal,哪怕加了json:"-",中间件或日志打印仍可能泄漏PasswordHash - 需要动态字段(如
fields=id,name)时,用map[string]any+ 白名单校验键名:if !validFields[fieldName] { continue },永远不用黑名单删key
X-Forwarded-For头没配SetTrustedProxies,或者没按真实网络拓扑填代理网段,会导致IP校验完全失效。这比白名单逻辑本身更容易出问题。


















