只要用对 database/sql 的占位符机制,SQL 注入在 Go 里可完全规避;db.Query 和 db.Prepare 均通过驱动将参数值隔离于 SQL 解析阶段,安全效果一致,区别仅在于执行计划是否复用;字段名、表名等标识符无法参数化,必须白名单校验。

只要用对 database/sql 的占位符机制,SQL 注入在 Go 里是可完全规避的——不是“降低风险”,而是“根本不会发生”。关键不在是否用了 ORM,而在于是否把用户输入当数据传,而不是当代码拼。
为什么 db.Query 和 db.Prepare 都能防注入,但行为不同?
两者都依赖数据库驱动对 ? 占位符的底层处理,参数值不会进入 SQL 解析阶段。区别在于执行路径:
-
db.Query("SELECT * FROM users WHERE id = ?", id):每次调用都走一次预编译流程(多数驱动会缓存执行计划) -
stmt, _ := db.Prepare("SELECT * FROM users WHERE id = ?"); stmt.Query(id):显式复用预编译语句,适合高频、固定结构查询 - 性能差异在高并发场景才明显;安全效果完全一致——只要你没手动拼接字符串
动态 WHERE 条件怎么写才安全?
不能拼接字段名或操作符,但可以拼接条件逻辑本身,前提是所有值仍走占位符:
- ✅ 安全做法:收集条件和参数两个切片,用
strings.Join()拼WHERE子句,再统一传参 - ❌ 危险写法:
"WHERE status = " + statusStr或"ORDER BY " + sortField - 字段/表名等标识符无法用占位符,必须白名单校验:
if !slices.Contains([]string{"created_at", "updated_at"}, sortField) { return err }
GORM 的 Raw 方法为什么还会中招?
db.Raw("SELECT * FROM users WHERE name = ?", name) 是安全的;但 db.Raw(fmt.Sprintf("SELECT * FROM %s", tableName)) 就是裸奔。
- GORM 不会自动拦截你传进去的拼接字符串,它只保证占位符部分被转义
- 常见翻车点:
db.Raw("UPDATE users SET status = ? WHERE id IN ("+idsStr+")", status)——idsStr若来自用户输入且未校验,就可能注入 - 正确替代:
db.Where("id IN ?", idSlice).Update("status", status),让 GORM 处理 IN 列表
最易被忽略的点:占位符只保值,不保标识符。表名、列名、排序方向(ASC/DESC)、UNION 结构这些,数据库层根本不允许参数化,必须靠代码层白名单或枚举约束。漏掉这一环,前面所有参数化都白做。











