真正安全的做法是值走占位符、结构走白名单;占位符(如?、$1)由驱动分离sql模板与参数,防注入;表名/字段名/order by等须白名单校验,不可参数化;gorm/sqlx的raw()和拼接仍危险;scan需严格类型匹配。

Go 语言里没有“字符串过滤机制”能防 SQL 注入——靠过滤是防不住的,而且容易漏、容易绕过。真正安全的做法只有一条:不让用户输入参与 SQL 结构生成。
database/sql 的 ? 和 $1 占位符为什么能防注入
MySQL 驱动识别 ?,PostgreSQL 驱动识别 $1、$2,这些是数据库协议层定义的参数绑定入口。驱动会把 SQL 模板和参数值分开发送给数据库:模板走语法解析,参数走绑定通道。哪怕你传 "1' OR '1'='1" 进 db.Query("SELECT * FROM users WHERE id = ?", input),它也只会被当做一个字符串字面量处理,不会改变 WHERE 条件逻辑。
-
db.Query("SELECT * FROM users WHERE name = ?", "admin' --")→ 安全,查的是名字叫admin' --的用户 -
db.Query(fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", input))→ 危险,input是admin' OR '1'='1就直接绕过认证 - 命名参数如
:name或@id不被 Go 标准库支持,会报错sql: expected 0 arguments, got 1
表名、字段名、ORDER BY 这些为啥不能用占位符
SQL 标准规定 FROM、ORDER BY、GROUP BY、LIMIT 等子句属于查询结构,必须在语句编译阶段确定,数据库不提供运行时绑定接口。所以 db.Query("SELECT * FROM ? WHERE id = ?", tableName, id) 必然 panic。
- 排序字段必须白名单校验:
validSortFields := map[string]bool{"created_at": true, "score": true},不在其中就拒掉 - 排序方向也得白名单:
if dir != "ASC" && dir != "DESC" { return errors.New("invalid sort direction") } - 表名映射建议用常量或 switch:
case "user_log": return "user_logs_2024",而不是"logs_" + input - 别信“转义单引号就能防”,
tableName = "users; DROP TABLE admins--"这种 payload 逃得过任何简单 replace
GORM 和 sqlx 的 Raw() 陷阱在哪
db.Raw() 只保证占位符部分安全,不拦截你拼进去的 SQL 片段。很多人以为用了 GORM 就高枕无忧,结果在 Raw() 里拼接表名或字段名,等于裸奔。
-
db.Raw("SELECT * FROM users WHERE status = ?", status).Scan(&u)→ 安全 -
db.Raw(fmt.Sprintf("SELECT * FROM %s", tableName)).Scan(&u)→ 危险,tableName来自用户就完蛋 -
db.Where("id IN (?)", idsSlice)→ GORM 自动展开为IN (?, ?, ?),安全;但db.Raw("SELECT * FROM users WHERE id IN (" + idsStr + ")")→idsStr若含子查询就触发注入 - sqlx 的
sqlx.In()是专门解决IN场景的工具,别自己拼逗号分隔字符串
Scan 和类型匹配容易被忽略的坑
参数化防住了注入,但 Scan 时类型不匹配会导致静默失败或 panic,间接破坏业务逻辑——比如本该拒绝的非法输入被“凑合”读进变量,后续判断就失效了。
- 数据库字段是
BIGINT,用int接可能溢出(32 位系统),优先用int64 - 字段允许
NULL,必须用sql.NullString、sql.NullInt64,否则Scan报错sql: Scan error on column index X: unsupported Scan - struct 字段名和数据库列名不一致时,sqlx 需加
db:"column_name"tag,否则反射写入失败,字段为空也不报错 - 字段未导出(小写首字母)→ 反射无法写入,看似成功实则数据丢失
最危险的不是不会写安全代码,而是误以为“过滤了特殊字符”或“用了 ORM”就安全了。只要用户输入以任何形式拼进 SQL 字符串,不管中间加了多少正则、replace 或 trim,都可能被绕过。真正的防线只有两条:值走占位符,结构走白名单。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











