database/sql 防注入关键在于参数化值和白名单校验标识符:占位符需匹配驱动(mysql用?,postgresql用$1),order by/表名/字段名等结构不可参数化,必须白名单校验;gorm raw() 仍需手动确保参数化,动态sql须拆解条件逻辑而非拼接字符串。

database/sql 本身不防注入,防注入靠的是你有没有把用户输入当“值”传进去——语言学习技巧在这里不是指背语法,而是建立对 SQL 执行阶段的直觉判断:哪些能参数化,哪些必须白名单。
占位符必须匹配驱动,写错等于没写
MySQL 驱动只认 ?,PostgreSQL 驱动只认 $1、$2,SQLite 虽两者都支持但行为不一致。写错不会编译报错,而是静默失效:
-
db.Query("SELECT * FROM users WHERE id = $1", 123)在 MySQL 下查不到数据($1被当字面量) -
db.Query("SELECT * FROM users WHERE id = ?", 123)在 PostgreSQL 下报sql: expected 0 arguments, got 1 - 跨库迁移时硬编码占位符,测试可能通过,线上却出问题
ORDER BY / 表名 / 字段名不能用占位符
db.Query("SELECT * FROM ? ORDER BY ?", tableName, sortField) 必然 panic:SQL 标准规定这些属于查询结构,数据库编译阶段就要确定,没有运行时绑定接口。
真正该做的是白名单校验:
- 排序字段:
validFields := map[string]bool{"created_at": true, "score": true, "name": true},查不到就拒掉 - 排序方向:
if dir != "ASC" && dir != "DESC" { return err },别信strings.ToUpper(dir)后直接拼 - 表名映射:用
switch或常量,比如tenantID == "prod"→ 查users_prod,而不是"users_" + tenantID
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若来自用户且未校验,idsStr = "1, (SELECT password FROM admins)"直接触发注入 - 正确做法:
db.Where("id IN ?", idSlice).Update("status", status),让 GORM 自动展开占位符 -
gosec规则G201会标记所有Raw()调用,需人工逐行确认是否带参数化
动态 WHERE 条件要拆成两层结构
不能拼字段名或操作符,但可以拼条件逻辑本身,前提是所有值仍走占位符:
- ✅ 安全:
conds := []string{"status = ?"}; params := []interface{}{status}; if from != "" { conds = append(conds, "created_at >= ?"); params = append(params, from) },最后db.Query("SELECT * FROM users WHERE "+strings.Join(conds, " AND "), params...) - ❌ 危险:
"WHERE status = " + statusStr或"ORDER BY " + sortField - 漏掉标识符校验(如
sortField)比漏掉值参数化更危险——前者让整个参数化防线失效
白名单校验不是可选项,是最后一道不可绕过的关卡;只要用户输入参与了 SQL 结构生成(哪怕只拼了一个字段名),前面所有占位符都白做。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











