表名、字段名、ORDER BY等SQL结构部分不可用占位符,必须白名单校验;Raw()/Exec()只转义参数值,不拦截拼接的SQL片段,手动拼接用户输入即失守。

Raw() 和 Exec() 里的表名、字段名不能用 ? 占位符
GORM 的 Raw() 和 Exec() 方法只对 ? 或 $1 等占位符位置的参数做转义,**不拦截你拼进去的 SQL 片段**。写成 db.Raw("SELECT * FROM " + tableName + " WHERE id = ?", id),只要 tableName 来自用户且未校验,就直接失守。
数据库协议层根本不允许运行时绑定表名或字段名——db.Raw("SELECT * FROM ? WHERE id = ?", tableName, id) 会 panic 或被忽略,不是 GORM 不支持,是 MySQL/PostgreSQL 协议本身禁止。
- 必须用白名单映射:比如按租户动态选表,用
switch tenant { case "prod": table = "users_prod" },而不是"users_" + tenant - 字段名同理:若需动态
SELECT字段,先定义validFields := map[string]bool{"id": true, "name": true, "email": true},再校验 - 别信“加了单引号就安全”:
tableName = "users; DROP TABLE admins--"这类 payload 能绕过所有简单字符串替换
IN 子句、WHERE 条件别手动拼接字符串
常见错误是把用户传入的 ID 列表拼成 "1,2,3" 再塞进 Raw():db.Raw("UPDATE users SET status = ? WHERE id IN (" + idsStr + ")", status)。攻击者传 "1, (SELECT password FROM admins)" 就能拖库。
正确做法是让 GORM 自动展开参数:
-
db.Where("id IN ?", idSlice).Update("status", status)——idSlice是[]uint或[]string,GORM 会生成IN (?, ?, ?)并绑定 - 若必须用
Raw(),改用sqlx.In配合db.Raw().Scan(),但前提是In的参数值仍是干净的 - 动态条件组合建议拆成两层:
conds := []string{"status = ?"}; params := []interface{}{status},再追加校验过的子句
ORDER BY、GROUP BY、HAVING 中的结构部分必须白名单校验
Order("created_at " + sortDir) 看似无害,但若 sortDir 是 "DESC; DROP TABLE users --",整条语句就失控。GORM 的 Order() 方法不解析占位符,Order("name = ?") 会原样塞进 SQL,等于主动关掉防护。
- 排序字段和方向要分开校验:
if !validSortFields[field] || (dir != "ASC" && dir != "DESC") { return err } -
GROUP BY字段同样不可参数化:db.Group(groupField)前必须确认groupField在白名单内(如"status","category") -
HAVING子句里只有值能走占位符,字段名、函数名(COUNT,AVG)、操作符(>,=)都属于结构,必须硬编码或白名单
gosec G201 规则不是摆设,要人工逐行确认
静态扫描工具 gosec 的 G201 规则会标记所有 Raw() 和 Exec() 调用,但它不会告诉你哪一行拼错了——它只提醒“这里可能裸奔”。很多团队扫到就忽略,等上线后被扫出 SELECT * FROM users; DROP TABLE admins 才反应过来。
- 每个
Raw()调用都要回答三个问题:表名/字段名/排序方向是否来自白名单?有没有手动拼接字符串?占位符是否只用于值? - 特别警惕带注释的拼接:
"WHERE name = '" + name + "' /* " + comment + " */",注释内容也可能含恶意 payload - 测试时故意传畸形输入(如
"); DROP TABLE users--),看日志是否出现语法错误或非预期查询
真正危险的从来不是 ? 没写对,而是你忘了自己正在手写 SQL。只要进了 Raw() 或 Exec(),GORM 的 ORM 层防护就全部失效,每一分拼接都得自己扛。

















