参数化查询占位符必须与数据库驱动严格匹配,MySQL用?、PostgreSQL用$1,混用会导致SQL注入;GORM的Raw()等原生SQL接口需严格参数化,动态字段名须白名单校验,html/template仅防HTML上下文XSS,JS/CSS等需额外防护。

database/sql 参数化查询占位符写错就等于没防
占位符不是通用的,? 和 $1 不能混用,写错不会报错,但参数会被当字面量传入——等价于字符串拼接。
MySQL 驱动只认 ?,写成 $1:查询永远无结果,或意外匹配到字段值为 "$1" 的记录;PostgreSQL 驱动只认 $1、$2,写成 ?:驱动直接报错 sql: expected 0 arguments, got 1 或静默丢弃参数。
- 跨数据库迁移时硬编码占位符,会导致运行时行为突变,建议封装统一的
QueryBuilder抽象层 - 别依赖 ORM 的“默认安全”幻觉——GORM 的
Raw()、Exec()、Session().Exec()一调用,所有防护自动失效 - 错误示例:
db.Raw("SELECT * FROM users WHERE name = '" + name + "'").Find(&users) - 正确做法:
db.Where("name = ?", name).Find(&users)或db.Raw("SELECT * FROM users WHERE name = ?", name).Find(&users)
GORM 原生 SQL 接口是逃生舱口,不是后门
Raw() 方法本身不危险,危险的是把它当成“可以放心拼接”的借口。只要用户输入进了 SQL 字符串字面量,就已失守。
动态表名、字段名、ORDER BY 子句无法参数化,必须白名单校验。比如只允许 name、email、created_at 出现在排序字段中,其他一律拒绝。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- gosec 扫描规则
G201会标记所有Raw()调用,需人工逐条确认是否带参数化 - 不要以为加了
Where()就安全——Where("id = ?", id).Raw("...")中的Raw()仍独立承担风险 - 用
fmt.Sprintf拼接 SQL 是高危红线,连测试代码里都该禁用
html/template 只保 HTML 上下文,JS/CSS/URL 里照样 XSS
html/template 对 {{.UserName}} 自动转义成 <script>,但它只在 HTML 元素内容中生效。一旦变量进到 JS 字符串、href、src、style 或 innerHTML,它完全不起作用。
- 危险写法:
<script>var name = "{{.UserName}}";</script>—— 输入"); alert(1); //直接逃逸执行 - 安全写法:
<script>var name = {{.UserNameJSON}};</script>,其中UserNameJSON是json.Marshal后的字符串 - 富文本不能靠
template.HTML放行,必须用bluemonday白名单净化,禁用script、onerror、javascript:等执行向量 - HTTP 响应头仍需设置
Content-Security-Policy: default-src 'self',这是防止 inline script 执行的最后一道防线
输入验证 ≠ 防注入,类型转换只是辅助手段
很多人以为 strconv.Atoi 转完整数就防住了 SQL 注入,这是错觉。攻击者可以绕过类型检查,比如在 URL 参数中传 id=1%20OR%201=1(空格被编码),后端若未做严格校验,仍可能拼进 SQL。
- 数值型输入要做范围检查(如 ID > 0 && ID
- 字符串输入要限制长度、过滤控制字符、拒绝含 SQL 关键字的组合(如
UNION SELECT) - 别把防御逻辑分散在各处——统一收口到中间件或请求解析层,避免遗漏
- 注意:
html.EscapeString是输出编码补救手段,不是替代html/template的方案
真正难防的从来不是技术本身,而是动态标识符校验的粒度、原生 SQL 使用边界的模糊地带,以及开发时那句“这个字段肯定不会被恶意构造”的自我安慰。

















