动态sql在gorm中必须手动防护:表名、字段名、排序方向等标识符需白名单校验,用户输入的值必须用?或$1占位符;select字段、from表名、order by等sql骨架不可参数化,raw()安全前提是拼接前完成校验、值走占位符。

动态 SQL 在 GORM 中无法靠“自动防护”兜底,必须手动拆解结构与值——表名、字段名、排序方向等标识符一律白名单校验,所有用户输入的值必须走 ? 或 $1 占位符,Raw() 不是快捷方式,而是高危入口。
哪些部分根本不能用占位符?
SQL 的“骨架”部分数据库根本不允许参数化:SELECT 后的字段名、FROM 表名、ORDER BY 字段、GROUP BY 列、UNION 子句、甚至 ASC/DESC 排序方向——这些都不是“值”,驱动会直接报错,比如 sql: expected 0 arguments, got 1。
常见翻车点:
- 写
db.Raw("SELECT * FROM " + tableName + " WHERE id = ?", id):表名拼接即失守 - 写
db.Order("created_at " + sortDir):sortDir是"ASC; DROP TABLE users --"就完了 - 把 JSON 配置里的
"order_by": "name"直接塞进Order()而不校验
正确做法:
- 白名单用
map[string]bool快速判断:validSortFields := map[string]bool{"created_at": true, "name": true, "score": true} - 表名映射走常量或 switch:
switch tenant { case "prod": table = "users_prod"; case "dev": table = "users_dev" } - 排序方向硬编码:
if sortDir == "desc" { db.Order("name DESC") } else { db.Order("name ASC") }
Raw() 怎么用才不算裸奔?
Raw() 绕过 GORM 所有 ORM 层检查,直连底层 driver。它本身不危险,危险的是你传给它的字符串里混了未校验的用户输入。
安全边界只有一条:**所有动态拼接的部分,必须在进入 Raw() 前完成白名单校验或硬编码映射;所有值,必须用 ? 或 占位符传参。**
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
- ❌ 错误:
db.Raw("SELECT * FROM " + inputTable + " WHERE status = '" + status + "'").Scan(&u) - ✅ 正确:
if !validTables[inputTable] { return errors.New("invalid table") }; db.Raw("SELECT * FROM ? WHERE status = ?", inputTable, status).Scan(&u)(注意:GORM 不支持?替换表名,此写法仅作示意;实际需用Table()或白名单后拼接) - ✅ 更稳妥:
db.Table(sanitizedTable).Where("status = ?", status).Scan(&u),把表名校验和 ORM 查询分离
PostgreSQL 用户注意:Raw() 中的 $1 占位符只对值有效,ORDER BY $1 会报错,必须用白名单 + 字符串拼接(但拼接前已校验)。
动态 WHERE 条件怎么拼才不出错?
别试图用 Struct 自动展开所有条件——零值字段被跳过、IN 不支持、字段名错配静默失效。真实业务中,条件往往是可选的、组合的、带运算符的。
推荐显式构建查询结构:
- 用切片收集条件子句:
conds := []string{"deleted = ?"}; args := []interface{}{false} - 按需追加:
if name != "" { conds = append(conds, "name LIKE ?"); args = append(args, "%"+name+"%") } - 最终拼接:
db.Where(strings.Join(conds, " AND "), args...).Find(&users)
关键约束:
- 每个
?对应一个具体类型值,禁止传interface{}或sql.RawBytes -
IN必须用db.Where("id IN ?", ids),GORM 会自动展开为IN (?, ?, ?),别手写IN (" + strings.Join(...) + ")" - Struct 传参仅适用于“全字段确定、无零值干扰”的简单场景,复杂动态条件别依赖它
Scan 接收时容易被忽略的静默陷阱
Scan 不防注入,但它错配字段顺序或类型,会导致数据写进错误变量、NULL panic、甚至把恶意字段名当值用——这虽不是传统注入,但可能引发越权或服务崩溃。
- 永远显式写
SELECT字段:SELECT id, name, email, created_at FROM users,不用SELECT * - 接收用结构体,字段顺序与
SELECT严格一致;若用Scan(&id, &name, &email),改表加字段就崩 - 任何可能为
NULL的列,必须用sql.NullString、sql.NullInt64等类型接收,普通string遇 NULL 直接 panic - 多人协作时,建议对关键查询加单元测试,固定字段顺序与类型映射
最易被忽略的一点:白名单校验做完,Raw() 和 Scan() 之间还隔着一层——你写的 SQL 字段顺序,必须和 Scan 接收顺序、结构体字段声明顺序三者完全一致,差一个就错位,且往往不报错,只写错数据。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










