go 1.22 不新增 sql 注入防护机制,防注入完全依赖正确使用参数化查询、白名单校验非值部分及避免字符串拼接;占位符须匹配驱动类型,标识符(如表名、order by)不可参数化,必须白名单校验;gorm raw() 不豁免拼接风险,scan 需适配 null 类型。

Go 1.22 本身不新增 SQL 注入防护机制,防注入完全取决于你是否正确使用 database/sql 的参数化能力——用对占位符、禁掉字符串拼接、白名单校验非值部分,就安全;写错一个 ? 或拼一次 tableName + input,就裸奔。
占位符必须匹配驱动类型,混用直接失效
Go 1.22 的 database/sql 接口没变,但底层驱动仍严格区分占位符语法:
- MySQL/SQLite 驱动只认
?:写成$1不报错,但参数被忽略,查不到数据或意外匹配字面量$1 - PostgreSQL 驱动(如
lib/pq或pgx)只认$1、$2:写成?会 panic,错误信息通常是sql: expected 0 arguments, got 1或ERROR: syntax error at or near "123" - 同一个项目里混用(比如 MySQL 连接里写
$1)不会提前报错,运行时才出问题,且现象隐蔽——看似查不到数据,实则是参数根本没进查询
ORDER BY / 表名 / 字段名不能参数化,白名单是唯一解
SQL 标准禁止参数化标识符(identifier),所有驱动在遇到 ORDER BY ? 时都会 panic,错误是 sql: expected 0 arguments, got 1。这不是 Go 版本问题,是数据库协议限制。
- 排序字段必须硬编码白名单:
map[string]bool{"created_at": true, "name": true, "score": true},不在其中就拒掉 - 表名映射别用字符串拼接:
"users_" + tenantID是高危操作;改用switch tenantID { case "prod": return "users_prod" }或常量映射 - 排序方向(
ASC/DESC)同样要白名单校验:if dir != "ASC" && dir != "DESC" { return errors.New("invalid sort direction") },别信strings.ToUpper(dir)后直接拼 -
LIMIT、OFFSET、GROUP BY同理,不支持参数化,必须走校验+拼接
GORM 的 Raw() 不是安全豁免,拼接表名照样中招
GORM 在 Go 1.22 下默认开启 PrepareStmt,但 Raw() 方法只保证占位符部分被转义,对拼接部分完全不干预。
- 安全写法:
db.Raw("SELECT * FROM users WHERE name = ?", name).Scan(&u) - 危险写法:
db.Raw("SELECT * FROM " + tableName + " WHERE id = ?", id).Scan(&u)——tableName若来自用户输入且未白名单校验,就是典型注入入口 - 更安全的替代:
db.Table(tableName).Where("id = ?", id).Find(&u),前提是tableName来自枚举或配置项,不是动态拼接 - 禁用
Raw()场景:从配置表读 SQL 模板再用fmt.Sprintf填充——这和手写拼接没区别,只是绕了层壳
Scan 接收 NULL 时类型不匹配会 panic,不是注入但危害相当
Scan 本身不参与防注入,但它处理 NULL 字段时若类型不对,会直接 panic 或静默丢数据,暴露结构或引发逻辑异常。
- 数据库字段允许 NULL,就必须用
sql.NullString、sql.NullInt64等接收,不能直接用string或int - 结构体字段顺序必须与
SELECT列顺序严格一致,否则Scan会错位赋值,导致数据污染 - 用
Rows.Columns()动态获取列名再Scan虽灵活,但增加复杂度和出错概率,普通场景建议固定列名 + 显式类型
最易被忽略的点:所有“动态”部分——哪怕只是 tenant_id 决定查哪个分表,只要没走白名单或预定义映射,就等于把 SQL 结构交给用户控制。Go 1.22 不会帮你拦住这个,它只管参数绑定是否合规。











