SQL注入防护核心是参数化查询:用?或$1占位符传参,禁用字符串拼接;ORDER BY/表名/字段名须白名单校验;GORM Raw()仅占位符安全;NULL字段需用sql.Null类型Scan,结构体字段顺序须与SELECT一致。

db.Query 和 db.Exec 必须用 ? 或 $1,不能拼字符串
Go 本身不防注入,防注入靠的是你有没有把用户输入当“值”传进去。只要写 db.Query("SELECT * FROM users WHERE name = '" + name + "'"),就等于把数据库钥匙塞给攻击者。
正确做法是让驱动接管参数绑定:db.Query("SELECT * FROM users WHERE name = ?", name)(MySQL/SQLite)或 db.Query("SELECT * FROM users WHERE name = $1", name)(PostgreSQL)。驱动会把 SQL 模板和参数分开发送,用户输 admin' OR '1'='1 也只会被当做一个字符串值,不会触发语法解析。
- 占位符写错(比如 MySQL 里写
$1)不会报错,但参数会被忽略,查不到数据或意外匹配字面量$1 -
fmt.Sprintf、strconv.Itoa、+拼接一律禁止,哪怕加了单引号也没用 -
db.Query和db.Prepare安全性完全一致,区别只在是否复用执行计划;高频查询用Prepare省点开销,普通场景用Query更直白
ORDER BY / 表名 / 字段名必须走白名单校验
db.Query("SELECT * FROM users ORDER BY ?", "name") 会直接 panic:sql: expected 0 arguments, got 1。SQL 标准规定这些属于查询结构,数据库编译阶段就要确定,没法参数化。
真正该做的,是提前定义合法值并严格比对:
立即学习“go语言免费学习笔记(深入)”;
- 排序字段:用
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)"就触发注入 - 正确做法:用
sqlx.In或 GORM 的Where("id IN ?", idsSlice),让框架自动展开占位符 - 动态表名必须走
db.Table(tableName),且tableName必须来自枚举或配置,不能是用户直传
Scan 接收 NULL 和字段顺序容易引发 panic,不是注入但一样让服务崩
Scan 不参与防注入,但它出错会暴露结构、引发 panic,甚至间接导致逻辑异常。
- 数据库字段可能为
NULL,却用普通string接收 →panic: sql: Scan error on column index 0: unsupported Scan, storing driver.Value type <nil> into type *string - 所有可能为
NULL的字段,必须用sql.NullString、sql.NullInt64等类型接收 - 扫描到结构体时,字段顺序必须和
SELECT列严格一致;用SELECT id, name AS username就得对应结构体字段Username string
微服务里每个服务都独立连库,表名、租户前缀、排序策略往往随上下文变化——这些地方恰恰最易漏掉白名单校验,也最容易因 Scan 类型错配在上线后突然 panic。别指望日志里那句 executing: SELECT ... 能帮你发现问题,它只会把拼接痕迹原样打出来。


















