ORDER BY、GROUP BY、LIMIT、OFFSET、表名、字段名等SQL标识符绝对不能参数化,必须通过精确白名单校验后拼接,因数据库协议和Go驱动不支持对标识符使用?或$1占位符。

动态 SQL 中的 ORDER BY、GROUP BY、LIMIT、OFFSET、表名、字段名等,根本不能用 ? 或 $1 参数化——这不是 Go 写得不对,是数据库协议层就不支持。硬塞进去只会 panic 或静默出错,而攻击者早就能靠 score\u200b ASC; DROP TABLE users 这类 payload 绕过正则和空格过滤。
为什么白名单必须是“精确比对”,不能靠正则或替换
常见错误是写 regexp.MustCompile(`^[a-zA-Z_]+$`) 或 strings.ReplaceAll(s, "'", "''"),但攻击者用零宽空格(\u200b)、SQL 注释(/**/)、换行符甚至 Unicode 下划线(\uFF3F)就能绕过。这些字符在正则里不匹配,替换也漏掉,但数据库解析器照常识别。
- ✅ 安全做法:只允许枚举值,比如
validFields := map[string]bool{"name": true, "email": true, "created_at": true},再用if !validFields[field] { return errors.New("invalid field") } - ❌ 危险做法:任何基于“过滤”或“清理”的逻辑,包括
strings.TrimSpace、strings.Map、unicode.IsLetter组合 - 排序方向(
ASC/DESC)同样要白名单,不能拼fmt.Sprintf("ORDER BY %s %s", field, dir)——dir若为"DESC; --"就直接触发多语句
表名和数据库名拼接前必须做两级校验
仅查 INFORMATION_SCHEMA.TABLES 不够安全:一是耗性能,二是某些驱动不支持元数据查询;二是即使存在,也不能说明它就该被当前用户访问。更可靠的是硬编码映射 + 输入约束。
- ✅ 推荐方式:用
switch或map显式映射,例如input == "user" → "users_prod",不在映射中就拒掉 - ❌ 禁止写法:
"users_" + input或fmt.Sprintf("logs_%s", dateStr)——input = "prod; DROP TABLE logs_prod"会直接执行 - 若必须支持租户动态库名,需配合 DB 权限最小化:每个租户账号只能连指定库,且禁止
CREATE/DROP权限
sqlx.In 和 GORM 的 IN 查询怎么避免参数展开失败
sqlx.In 和 GORM 的 IN ? 看似自动处理 slice,但底层仍是占位符展开。传错格式会报 sql: expected 0 arguments, got 1 或静默漏数据。
立即学习“go语言免费学习笔记(深入)”;
- ✅ 正确用法:
query, args, _ := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids),再query = sqlx.Rebind(sqlx.MySQL, query)(MySQL)或sqlx.Rebind(sqlx.PostgreSQL, query) - ❌ 错误写法:
sqlx.In("SELECT * FROM users WHERE id IN (?)", ids)直接执行——?只代表一个占位符,ids是 slice,驱动无法自动展开 - GORM 同理:
db.Where("id IN ?", ids).Find(&users)✅;db.Where("id IN (?)", ids)❌(括号里只有一个?,不是模板)
最易被忽略的一点:白名单校验必须发生在 SQL 拼接之前,且不能和日志、监控、缓存等旁路逻辑耦合——一旦某个中间件偷偷把原始输入转成大写或去空格再传给校验函数,白名单比对就失效了。


















