参数化查询占位符必须与数据库驱动严格匹配:MySQL用?,PostgreSQL用$1,混用会导致SQL注入;动态字段需白名单校验;Raw()等原生SQL接口须始终参数化;HTML/JS上下文需不同转义策略;微服务间也需输入验证。

database/sql 参数化查询占位符写错就等于没防
占位符不是通用的,? 和 $1 不能混用,写错不会报错,但参数会被当字面量传入——等价于字符串拼接。
MySQL 驱动只认 ?,写成 $1:查询永远无结果,或意外匹配到字段值为 "$1" 的记录;PostgreSQL 驱动只认 $1、$2,写成 ?:驱动直接报错 sql: expected 0 arguments, got 1 或静默丢弃参数。
- 跨数据库迁移时硬编码占位符,会导致运行时行为突变
- 建议封装统一的
QueryBuilder抽象层,按驱动类型自动适配占位符 - 别依赖 ORM 的“默认安全”幻觉——
GORM的Raw()、Exec()、Session().Exec()一调用,所有防护自动失效
GORM 原生 SQL 接口是逃生舱口,不是后门
Raw() 方法本身不危险,危险的是把它当成“可以放心拼接”的借口。只要用户输入进了 SQL 字符串字面量,就已失守。
错误示例: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)
- 动态表名、字段名、
ORDER BY子句无法参数化,必须白名单校验(如只允许name、email、created_at) -
gosec扫描规则G201会标记所有Raw()调用,需人工逐条确认是否带参数化 - 不要以为加了
Where()就安全——Where("id = ?", id).Raw("")中的Raw()仍独立承担风险 - 用
fmt.Sprintf拼接 SQL 是高危红线,连测试代码里都该禁用
html/template 只保 HTML 上下文,JS/CSS/URL 里照样 XSS
html/template 对 {{.UserName}} 自动转义成 <script></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 执行的最后一道防线
微服务间调用也要防注入和 XSS
内部服务通信常被误认为“可信”,但攻击者可通过边界网关或上游服务污染数据流。一个未校验的 user_id 字段,可能从 API 网关透传到下游订单服务,再拼进日志 SQL 或渲染到管理后台页面。
- 每个微服务入口必须做输入验证,不能只靠网关层拦截
- 下游服务接收 JSON 或 gRPC 请求时,对关键字段(如
order_id、redirect_url)强制白名单或正则校验 - 跨服务传递的 HTML 片段(如通知模板)必须经
bluemonday二次净化,不能复用上游已处理结果 - 日志打印用户输入前,需过滤控制字符和双引号,避免日志注入伪造条目
最麻烦的从来不是“会不会写参数化查询”,而是动态字段、多数据库兼容、服务间信任边界模糊这些点——它们不报错,但会在某个上线后的深夜突然触发漏洞。











