必须严格匹配驱动占位符:MySQL用?、PostgreSQL用$1,混用或写错将导致参数被当字面量处理,等同于字符串拼接;GORM Raw()不自动防护,动态标识符如表名、ORDER BY字段必须白名单校验后拼接。

database/sql 参数化查询必须匹配驱动占位符
Go 的 database/sql 本身不解析 SQL,占位符语义完全由数据库驱动决定。写错占位符不会报错,而是把参数当字面量传入——等价于字符串拼接,注入风险直接回归。
- MySQL 驱动只认
?:写成$1→ 查询永远无结果,或意外匹配到字段值为$1的记录 - PostgreSQL 驱动只认
$1、$2:写成?→ 报错sql: expected 0 arguments, got 1或静默丢弃参数 - SQLite 驱动两者都支持,但行为不一致:同一段代码在不同环境表现可能不同
- 跨数据库迁移时硬编码占位符,会导致运行时行为突变,且难以通过单元测试覆盖
建议统一用封装层屏蔽差异,或直接选用 GORM 这类 ORM;若必须手写 SQL,按目标数据库严格校验占位符类型。
GORM Raw() 和 Exec() 是“逃生舱口”,不是安全接口
GORM 默认使用参数化查询,但它的 Raw()、Session().Exec()、DB.Exec() 等原生 SQL 接口一旦调用,所有防护自动失效。很多人误以为加了 Where() 就安全,却在 Raw() 里又拼接变量。
- ❌ 危险:
db.Raw("SELECT * FROM users WHERE name = '" + name + "'").Find(&users) - ✅ 安全:
db.Raw("SELECT * FROM users WHERE name = ?", name).Find(&users)或更推荐的db.Where("name = ?", name).Find(&users) -
gosec扫描规则G201会标记所有Raw()调用,需人工逐行确认是否带参数化 - 动态表名、字段名、
ORDER BY、LIMIT子句无法参数化,必须白名单校验后拼接
html/template 只对 HTML 内容转义,JS/CSS/URL 上下文要单独处理
html/template 的自动转义只在 HTML 文本节点生效。一旦用户输入进入 JavaScript 字符串、CSS 属性、URL 参数或 innerHTML,它完全不起作用。
立即学习“go语言免费学习笔记(深入)”;
- ❌ 危险:
<script>var name = "{{.UserName}}";</script>→ 输入"); alert(1); //直接触发 XSS - ✅ 安全:
<script>var name = {{.UserNameJSON}};</script>,其中UserNameJSON是json.Marshal后的字符串(注意类型为template.JS) - 富文本不能靠
template.HTML放行:必须用bluemonday白名单净化,禁用script、onerror、javascript:等执行向量 - HTTP 响应头仍需设置
Content-Security-Policy: default-src 'self',这是防止 inline script 执行的最后一道防线
中间件是高频注入入口,日志和请求头处理最容易翻车
很多线上 SQL 注入/XSS 漏洞不出现在业务 Handler,而出现在中间件。因为中间件是每个请求必经之路,且常被当成“辅助逻辑”忽略安全校验。
- 日志中间件直接拼接
X-Forwarded-For或User-Agent到日志字符串 → 可注入恶意 payload 到日志系统甚至 ELK 渲染页 - 认证中间件读取
Authorization头后未校验格式,直接传给下游 → 可伪造 token 或注入控制字符 - 限流中间件用
r.URL.Query().Get("user_id")构造缓存 key → 若未过滤特殊字符,可能污染 Redis 键空间或触发命令注入 - 所有中间件中涉及字符串拼接、模板渲染、Header 解析、Body 解码的地方,都要当作用户输入一样做验证和转义
最易被忽略的是:中间件里对 context.WithValue 存入的数据,后续 Handler 可能未经检查直接用于 SQL 或 HTML 渲染——这个隐式数据流比显式参数更危险。


















