所有动态where条件必须分离结构与值:条件模板硬编码或白名单生成,参数值统一用?占位符;order by、表名等不可参数化部分须白名单校验,否则导致sql注入或逻辑绕过。

所有动态 WHERE 条件必须拆解为“条件表达式字符串 + 参数值列表”,不能拼字段名、操作符或表名;GORM 2 的 Where 方法只对 ? 占位符后的值做转义,对前面的 SQL 片段完全不处理。
Why Where("name = '" + name + "'") 是裸奔
这种写法把用户输入直接塞进 SQL 字符串里,name 若为 admin' OR '1'='1,最终执行的就是 WHERE name = 'admin' OR '1'='1'——条件恒真。GORM 不会拦截、不报错、不警告,只照单执行。
-
Where(fmt.Sprintf("status = %s", status))同理危险,哪怕加了单引号也无效 -
Where("id IN (" + idsStr + ")")是高危翻车点:若idsStr = "1, (SELECT password FROM admins)",直接触发子查询注入 - GORM 的日志(
Logger)打印的是带占位符的 SQL,不是真实执行语句,复制它去数据库里跑会出错
安全构建 WHERE 的三步法
核心是把“结构”和“值”彻底分离:条件模板用硬编码或白名单生成,参数值统一走 ? 占位符。
- 用切片收集条件片段:
conds := []string{"status = ?"},再根据业务逻辑append其他合法条件,如"age > ?" - 同步维护参数值切片:
params := []interface{}{status},每次append对应值,如age - 最终调用:
db.Where(strings.Join(conds, " AND "), params...)——params...会展开为多个独立参数,由驱动绑定
IN 子句和 NULL 字段的坑
IN 看似简单,但 GORM 2 对 slice 的展开有隐含规则:必须传 slice 本身,不能先转成字符串。
- ✅ 安全:
db.Where("id IN ?", []uint{1, 2, 3})→ 自动转为IN (1,2,3) - ❌ 危险:
db.Where("id IN (?)", "1,2,3")→ 当成单个字符串字面量,查不到数据 - NULL 字段必须显式处理:若数据库列允许 NULL,结构体字段要用
sql.NullString或*string接收,否则Scan会 panic,导致后续权限判断失效
哪些地方永远不能参数化,必须白名单
SQL 标准规定,ORDER BY、GROUP BY、表名、字段名、LIMIT、OFFSET 都属于查询结构,在数据库编译阶段就必须确定,无法运行时绑定。
-
db.Order(fmt.Sprintf("name %s", dir))是典型错误——dir必须白名单校验:if dir != "ASC" && dir != "DESC" { return err } - 排序字段也得白名单:
validSortFields := map[string]bool{"created_at": true, "score": true},查不到就拒掉 - 动态表名别用
"users_" + tenantID,改用switch tenantID { case "prod": table = "users_prod" }
最易被忽略的点是:字段名拼错、结构体 tag 写错、NULL 处理遗漏,这些不会导致注入,但会让扫描静默失败或 panic,进而让业务逻辑(比如越权检查)绕过——攻击者不需要注入 SQL,只要让代码“不执行该执行的逻辑”就够了。











