必须改掉所有字符串拼接SQL,改用参数化查询或白名单校验;GORM链式API默认安全但Raw/Exec/Order等需严防注入,表名字段名须白名单,IN查询用Where("x IN ?", slice)自动绑定。

直接改掉所有拼接字符串生成 SQL 的地方,用参数化查询或白名单校验。GORM 本身不自动防注入,它只在你用 Where、First 等链式方法时默认安全;但一旦用 Raw()、Exec() 或手动拼接,就等于把数据库钥匙交出去。
db.Raw() 中的字符串拼接必须砍掉
db.Raw() 不是“免检通道”,它只对占位符部分做转义,其余全是裸奔。
常见错误现象:
- db.Raw("SELECT * FROM " + tableName + " WHERE id = ?", id) —— 表名拼接,立刻中招
- db.Raw("UPDATE users SET status = ? WHERE id IN (" + idsStr + ")", status) —— idsStr 来自用户且未校验,可能含 (SELECT password FROM admins)正确做法:
- 表名/字段名/排序方向等结构类内容,必须走白名单映射:
validTables := map[string]bool{"users": true, "orders": true} if !validTables[tableName] { return errors.New("invalid table name") } - 占位符只用于值(
?或$1),且类型要匹配(比如整数别传字符串)
GORM 链式 API 大部分是安全的,但有例外
Where()、Order()、Limit() 这些默认使用参数绑定,但要注意:
- Order("name DESC") 安全;Order(userInput) 不安全 —— 字段名和方向不能直接来自用户
- Where("age > ?", age) 安全;Where("age > " + ageStr) 危险
- Find(&users, "name = ?", name) 安全;Find(&users, "name = '"+name+"'") 是裸奔
容易踩的坑:
- 把
fmt.Sprintf套在Where()里:Where(fmt.Sprintf("status IN (%s)", placeholders), args...)—— 占位符生成逻辑若失控,照样注入 -
Scopes()里藏了拼接逻辑,没审计就复用,隐患深埋
动态 IN 查询怎么写才安全
用户传入 ID 列表(如[]int{1,2,3})很常见,但直接拼成 "id IN (1,2,3)" 是高危操作。
正确姿势:
- 用 Where("id IN ?", ids),GORM 会自动展开为 WHERE id IN (?, ?, ?) 并绑定参数
- 如果 ids 是字符串切片(如 []string{"a","b"}),同样适用:Where("code IN ?", codes)
- 绝对不要自己拼 strings.Join + fmt.Sprintf 构造 IN 子句
性能提示:
-
IN参数超过几千个时,MySQL 可能报max_allowed_packet错误,此时需分批处理,而不是退回到字符串拼接
NULL 值和 Scan 顺序容易被忽略
这不是注入漏洞,但会导致查询逻辑错乱,间接放大风险: -Scan() 时结构体字段顺序必须和 SELECT 字段顺序严格一致,否则值错位,可能把 email 当成 password 赋值
- 对可能为 NULL 的字段,用 sql.NullString、sql.NullInt64 等类型接收,否则 GORM 会 panic 或静默丢数据
- Rows.Scan() 手动扫描时,尤其要注意 sql.Null* 类型和指针传递,漏掉一个 & 就扫不进值
真正难的不是写对一行 Where,而是确保所有边界路径(比如导出功能、后台批量操作、日志回溯查询)都没偷偷拼 SQL。只要有一个 Raw 没过白名单,或一个 Order 没校验,整个防护体系就形同虚设。

















