gorm默认不拦截无where的update/delete,因其设计上将条件构造权完全交给上层;防护需主动通过钩子校验sql中是否存在有效(含参数、非恒真)where子句,并严格白名单管控字段名、表名及raw语句。

GORM 本身不是“全表SQL执行防护器”,它默认不拦截无 WHERE 的 UPDATE 或 DELETE,也不会自动阻止全表扫描。所谓“防护”,必须靠你主动加钩子、写校验、设规范——否则一条 db.Model(&User{}).Updates(map[string]interface{}{"status": 1}) 就可能清空整张用户表。
为什么 GORM 不拦全表 UPDATE?
这不是疏忽,是设计选择:GORM 把条件构造权完全交给上层。它只认结构体主键字段(如 ID),一旦该字段为零值(0、""、nil),就退化成无条件语句;Where("1=1") 这类恒真条件也合法,不会触发警告。
-
Save()和Updates()对零值主键静默忽略,不报错也不提示 - 批量更新(如
Updates([]User{}))更危险,因为没显式Where调用时,GORM 默认按 slice 元素的主键更新——若主键全为 0,就是全表覆盖 - 关联操作(如
Preload("Profile").Save(&user))可能隐式触发无条件子查询,逻辑分散难审计
用 BeforeUpdateHook 拦住无效 WHERE
最直接的方式是在 SQL 执行前检查 Statement.SQL.String() 是否含有效 WHERE 子句。关键不是“有没有 WHERE”,而是“有没有带参数的、非恒真的 WHERE”。
- 接受:
WHERE id = ?、WHERE email = ? AND status = ?(含占位符且非恒真) - 拒绝:
WHERE 1=1、WHERE true、WHERE id IS NOT NULL(无索引支持时等效全表) - 注意:
db.Unscoped().Where("deleted_at IS NULL")这类也得进白名单,否则会被误判
示例代码片段:
db.Callback().Update().Before("gorm:update").Register("check-where", func(db *gorm.DB) {
if db.Statement.SQL.String() == "" {
return
}
sql := db.Statement.SQL.String()
if strings.Contains(sql, "WHERE") && !isValidWhereClause(sql) {
db.AddError(errors.New("unsafe UPDATE: WHERE clause is missing or trivial"))
}
})
Raw 和 Scan 是防护盲区,必须单独盯死
db.Raw() 和 db.Scan() 完全绕过 GORM 查询构建器,等于直接把字符串扔给数据库驱动。这里没有参数化、没有占位符、没有校验——拼错一个单引号就是注入,漏一个 WHERE 就是全表扫。
- ❌ 危险:
db.Raw("UPDATE users SET status = " + status).Exec() - ✅ 安全:
db.Raw("UPDATE users SET status = ? WHERE id = ?", status, id).Exec() - ⚠️ 注意:
db.Table("users").Select("name, email").Where("id IN (?)", ids).Scan(&results)中,ids必须是切片,不能是拼接字符串
字段名/表名动态拼接必须走白名单
GORM 从不校验字段名合法性。如果用户输入 field=age'; DROP TABLE users; --,再被拼进 db.Where(fmt.Sprintf("%s = ?", field), value),最终 SQL 就是 WHERE age'; DROP TABLE users; -- = ?——MySQL 会执行前半句。
- 排序字段(
Order(r.URL.Query().Get("sort")))、分组字段(Group("xxx"))、SELECT 字段列表都属于元信息,不能参数化 - 解决方案只有白名单:
allowedSortFields := map[string]bool{"id": true, "created_at": true, "status": true} - 表名同理,
db.Table(userInputTable)必须先查白名单或正则校验(如^[a-z_][a-z0-9_]*$)
真正难防的不是技术点,而是分散在各处的“看起来没问题”的写法:一个漏掉的 Where、一次临时加的 Raw、一段没校验的字段名拼接——它们各自独立看都像小问题,合起来就是生产事故。防护必须贯穿整个数据访问层,不能只靠某一个 Hook 或配置开关。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











