必须使用参数占位符(如?或$1)而非字符串拼接来防止SQL注入;sql.RawBytes仅用于读取二进制字段,不可用于拼接SQL;动态表名/字段名需白名单校验;ORM应禁用Raw()并启用PrepareStmt;JSON中的SQL片段同样需严格校验。

用 database/sql 的 Query 和 Exec 时必须带参数占位符
Go 原生 database/sql 包本身不拼接 SQL 字符串,但开发者一不小心就手写 fmt.Sprintf 或字符串拼接,直接把用户输入塞进查询里。这是最常见、最危险的入口。
正确做法永远是用 ? 占位符(MySQL/SQLite)或 $1(PostgreSQL),让驱动做参数绑定。数据库看到的不是“拼出来的字符串”,而是独立的值,根本不会当 SQL 解析。
- 错:
db.Query("SELECT * FROM users WHERE name = '" + name + "'")—— 输入' OR 1=1 --就完蛋 - 对:
db.Query("SELECT * FROM users WHERE name = ?", name)—— 驱动自动转义并隔离 - PostgreSQL 要用
$1、$2:db.Query("SELECT * FROM users WHERE id = $1 AND status = $2", id, status)
sql.RawBytes 不是用来绕过参数绑定的
有人以为 sql.RawBytes 是“原始数据容器”,能用来动态拼 SQL,其实它只是读取二进制字段(比如 BLOB)时的临时缓冲区类型,跟防注入毫无关系。拿它拼接查询等于自废防御。
常见错误场景:从配置表读 SQL 模板,再用 sql.RawBytes 拼上用户 ID——这和直接字符串拼接没区别,只是换了个难懂的写法。
立即学习“go语言免费学习笔记(深入)”;
-
sql.RawBytes只该出现在Scan接收列值时,比如读MEDIUMBLOB - 任何需要“动态表名”“动态字段名”的需求,都不能靠参数绑定解决,得走白名单校验或重构设计
- 真要拼表名?先查一遍
INFORMATION_SCHEMA.TABLES确认存在,再硬编码比对,别信任何用户输入
ORM(如 GORM)开启 PrepareStmt 并禁用原生 SQL 拼接
GORM 默认会 prepare 语句,但如果你调用 Session(&gorm.Session{PrepareStmt: false}) 或手动用 Raw() 执行字符串,防护就失效了。尤其 Raw() + Scan() 组合,极易写出带拼接的“伪安全”代码。
- 默认开启
PrepareStmt时,db.Where("name = ?", name).Find(&u)是安全的 - 但
db.Raw("SELECT * FROM " + tableName + " WHERE id = ?", id).Scan(&u)—— 表名未校验,照样中招 - 禁用
Raw(),改用Table(tableName):db.Table(tableName).Where("id = ?", id).Find(&u),前提是tableName来自枚举或配置项
JSON 字段里的 SQL 片段不是“免检区”
有些服务把查询条件存成 JSON(比如搜索过滤器),后端解析后拼成 SQL。这时候容易误以为“JSON 是数据,不是代码”,放松校验。但只要最终进了 db.Query 的字符串参数,就是注入温床。
- 解析 JSON 后得到的
map[string]interface{},不能直接fmt.Sprintf进 SQL - 字段名(如
"user_name")必须映射到预设白名单:validFields := map[string]bool{"name": true, "email": true} - 操作符(
"gt","like")也要限制范围,避免注入OR 1=1类逻辑 - 数值类值可直接参数化,字符串类值必须走
LIKE ?而非LIKE '%"+v+"%'
真正麻烦的从来不是语法怎么写,而是哪一层该信任输入、哪一层必须掐死拼接——边界模糊的地方,往往就是漏洞藏身的位置。


















