raw()中表名、字段名、order by字段、group by表达式、union子句等结构部分必须白名单校验;值部分须用驱动匹配的占位符(mysql用?,postgresql用$1)且不可混用。

直接用 db.Raw() 拼接用户输入就是裸奔,安全与否只取决于你有没有把表名、字段名、排序方向这些“结构部分”白名单校验过,而值部分是否用了 ? 或 $1 占位符。
Raw() 里哪些地方必须白名单校验?
db.Raw() 绕过 GORM 所有 ORM 层检查,但数据库协议根本不允许 ? 替代表名、列名、ORDER BY 字段、GROUP BY 表达式或 UNION 子句——这些只能靠代码硬约束。
- 表名动态拼接?先查白名单:
validTables := map[string]bool{"users": true, "orders": true},不在其中就return errors.New("invalid table") - URL 传来的
sort参数?只允许"created_at"、"name"、"id",用map[string]bool快速判断 - 动态
GROUP BY字段?同理,不能靠strings.ReplaceAll(input, "'", "''")这种黑名单过滤,PostgreSQL 支持 Unicode 注释符、$1变体,轻松绕过
Raw() 中的值怎么传才安全?
值(value)部分必须用占位符,且占位符类型要和驱动严格匹配:MySQL 用 ?,PostgreSQL 用 、,混用会导致参数被当字面量处理,等同于字符串拼接。
- ✅ 安全:
db.Raw("SELECT * FROM users WHERE name = ? AND age > ?", name, age).Scan(&u)(MySQL) - ✅ 安全:
db.Raw("SELECT * FROM users WHERE name = $1 AND age > $2", name, age).Scan(&u)(PostgreSQL) - ❌ 危险:
db.Raw("SELECT * FROM users WHERE name = '" + name + "'").Scan(&u)—— 单引号被绕过,name是"admin' OR 1=1 --"就完蛋 - ❌ 崩盘:
db.Raw("SELECT * FROM users WHERE name = ? AND status = $1", name, status)—— 混用占位符,驱动可能忽略第二个参数或报错
IN 子句和空切片怎么写不翻车?
IN 看似简单,但手写 Raw() 时容易触发注入或 panic;GORM 的 Where("id IN ?", ids) 能自动展开并兼容空切片,Raw() 不行。
- ✅ 安全:
db.Raw("SELECT * FROM users WHERE id IN (?)", ids).Scan(&u)—— 注意是(?),不是(?...),GORM 会自动展开为(1,2,3)或跳过空切片 - ❌ 危险:
db.Raw("SELECT * FROM users WHERE id IN (" + idsStr + ")").Scan(&u)—— 若idsStr来自用户且含"1, (SELECT password FROM admins)",就中招 - ⚠️ MySQL 驱动不支持
IN (?)多值展开?那就得手动拼IN (?, ?, ?)并确保参数切片长度一致,否则sql: expected 3 arguments, got 2
Scan 接收结果时最容易被忽略的坑
Scan 本身不防注入,但它按 SELECT 后的**列顺序**和**类型**严格绑定变量。错配不会报注入,而是静默数据错乱甚至 panic。
- 永远显式写字段:
SELECT id, name, email FROM users,别用SELECT * - 接收优先用结构体,字段顺序必须与
SELECT严格对应;用多个&var容易漏掉新增列或顺序错位 - 可能为
NULL的字段,必须用sql.NullString或*string,普通string遇到NULL会 panic
真正难防的不是语法层面的注入,而是业务字段本该校验却没校验——比如前端传来的 status 值,没进白名单就直接塞进 WHERE status = ?,哪怕用了占位符,也等于把非法状态当合法条件执行。











