必须配对占位符与驱动:mysql/sqlite用?,postgresql用$1,混用会报错;表名、字段名等标识符须白名单校验,不可参数化;map取值传参前需类型断言,scan和日志也需防范间接注入。

用 db.Query 或 db.Prepare 时必须配对占位符
Go 的 database/sql 包本身不引入注入风险,但你写错占位符就会直接暴露漏洞。关键不是“用了预编译”,而是“占位符和驱动是否匹配”。
- MySQL / SQLite 驱动只认
?:写db.Query("SELECT * FROM users WHERE id = ?", id)安全;写$1会 panic 报pq: syntax error at or near "?"(即使你用的是 MySQL) - PostgreSQL 驱动只认
$1、$2:写?会报sql: expected 1 arguments, got 0 - 混用一定失败:
"WHERE name = ? AND status = $1"是无效 SQL,数据库直接拒掉 - SQLite 虽然两种都支持,但统一用
?更稳妥——避免将来换库时漏改
db.Raw() 和 GORM 原生 SQL 不自动校验表名
db.Raw() 是 GORM 里最常被误用的高危点。它不做任何字符串校验,只负责把 SQL 发给数据库。哪怕你用了 ? 占位符,只要表名、字段名、排序方向是拼进去的,就等于开门揖盗。
- 危险写法:
db.Raw("SELECT * FROM " + tableName + " WHERE id = ?", id).Scan(&user)——tableName一拼接,立刻失守 - 安全做法:白名单硬控表名:
validTables := map[string]bool{"users": true, "posts": true},查前先判if !validTables[tableName] { return errors.New("invalid table") } - 排序字段同理:
ORDER BY " + sortField + " " + sortDir可以,但sortDir必须严格限制为"ASC"或"DESC",不能靠替换单引号或分号来“过滤”
从 map[string]interface{} 取值传参前必须类型断言
Gin 接收表单或 JSON 后常转成 map[string]interface{},但这个 map 里的值可能是 string、float64 甚至 nil。如果直接塞进 SQL 占位符,GORM 或 database/sql 会把它当字符串拼进去——又退回到拼接逻辑。
- 错误示例:
v := data["id"]; db.Query("SELECT * FROM users WHERE id = ?", v)—— 若v是float64,底层可能转成字符串再拼,等效于手动拼接 - 正确做法:
if id, ok := data["id"].(int); ok { db.Query("...", id) },或更严谨地用strconv.Atoi转整型 - 尤其注意时间、布尔字段:JSON 里
true进来是bool类型,但某些 ORM 会把它 toString 成"true"再塞进 SQL,结果变成WHERE active = 'true'(字符串比较),而非布尔运算
Scan 和日志打印也是注入温床
很多人以为“只要 SQL 安全了就万事大吉”,但 Scan 和日志环节同样可能把恶意内容带进后续流程。它们不执行 SQL,但可能触发 XSS、命令注入或服务端模板渲染漏洞。
-
rows.Scan(&user.Name)如果user.Name后续被直接写进 HTML 响应,没做html.EscapeString,就是 XSS 入口 - 日志里打
log.Printf("user input: %s", userInput),若userInput含控制字符(如\x00、ANSI 转义序列),可能污染日志系统或终端显示 - GORM 的
db.Debug().Where(...).Find()会把完整 SQL 打进日志——如果 SQL 里有拼接痕迹,敏感字段(密码、token)就直接裸奔了
真正卡住 SQL 注入的,从来不是某个函数名或框架特性,而是你每一次拼字符串前有没有停下来问一句:“这玩意儿会不会进 SQL 解析器?” 表名、排序字段、LIMIT 数值、甚至 ORDER BY 后面的 ASC/DESC,只要没走参数化路径,就得靠白名单死守。











