必须用参数化查询防注入,占位符需匹配驱动规则(MySQL/SQLite用?,PostgreSQL用$1);表名、字段名、ORDER BY等标识符须白名单校验,不可参数化。

必须用参数化查询,别拼字符串;驱动不认错占位符会直接报错;ORDER BY/表名/字段名不能参数化,得走白名单校验。
database/sql 的 Query/Exec 必须带正确占位符
Go 标准库 database/sql 本身不防注入,它只负责传参——真正起作用的是你写的 SQL 字符串干不干净。MySQL 和 SQLite 驱动只认 ?,PostgreSQL 驱动只认 $1、$2。混用会立刻失败:
-
sql: expected 0 arguments, got 1:说明占位符没被识别,参数被当字面量丢进去了 -
ERROR: syntax error at or near "123":常见于 PostgreSQL 里误写?,驱动把它当普通字符解析了
实操建议:
- 查清你用的驱动(
github.com/go-sql-driver/mysql还是github.com/lib/pq),严格按其规则写占位符 - 永远用
db.Query("SELECT * FROM users WHERE id = ? AND status = ?", id, "active"),而不是fmt.Sprintf("... id = %d", id) - 参数类型要匹配:传
int64给?没问题,但传nil给非NULL字段会 panic,需用sql.NullInt64等显式类型
动态表名、字段名、ORDER BY 必须白名单校验
SQL 标准不允许参数化标识符(identifier),所有驱动都拒绝 ORDER BY ? 或 SELECT ? FROM users,运行时直接 panic。有人接着改成 fmt.Sprintf("ORDER BY %s", input),这就等于把数据库裸奔交出去了。
实操建议:
- 排序字段只允许从固定集合选:
validSortFields := map[string]bool{"created_at": true, "name": true, "score": true},查之前先判断validSortFields[input] - 表名映射用常量或 switch:
switch input { case "prod": tableName = "users_prod"; case "test": tableName = "users_test" },别拼"users_" + input -
LIMIT、OFFSET、GROUP BY同理,数值类参数可转int并做范围检查(如limit <= 100),字符串类必须白名单
GORM 等 ORM 也要小心 Raw() 和 Table() 的陷阱
GORM 默认开启 PrepareStmt,db.Where("name = ?", name).Find(&u) 是安全的。但一旦调用 Raw() 或手动拼接表名,防护就失效了。
实操建议:
- 禁用
Raw():除非绝对必要,否则不用;真要用,也只拼白名单内的字段或表名,参数仍走?或$1 - 用
Table(tableName)替代字符串拼接:db.Table(tableName).Where("id = ?", id).Find(&u),前提是tableName来自配置或枚举,不是用户输入 - 检查 GORM 日志是否输出原始 SQL——如果看到
SELECT * FROM users WHERE name = 'admin'' OR ''1''=''1'这种,说明参数没绑上,得回溯代码找拼接点
Scan 接收 NULL 时类型必须显式声明
Scan 不参与防注入,但它出错会暴露结构、引发 panic,甚至间接导致逻辑异常。比如数据库字段允许 NULL,但你用 string 接收,遇到 NULL 就 panic;或者用 *string 却没判空,后续解引用崩掉。
实操建议:
- 字段可能为
NULL时,必须用sql.NullString、sql.NullInt64、sql.NullBool等类型接收 - 扫描后检查
.Valid字段:if name.Valid { fmt.Println(name.String) },别直接用name.String - 结构体字段加
sqltag 时注意:没有omitempty的NULL字段会覆盖已有值,容易引发静默数据污染
最易被忽略的点是:白名单校验和参数绑定是两套机制,缺一不可。占位符只保数据安全,保不住标识符;白名单只保标识符安全,保不住数据语义。两者必须同时落地,漏一个就等于开了后门。

















