唯一合规防御方式是参数化查询:mysql用?、postgresql用$1,动态标识符须白名单校验,禁用字符串拼接与手动转义。

Go 中不要手动转义 SQL 特殊字符
直接对用户输入做 strings.Replace 或正则替换引号、分号、-- 等,是危险且无效的。SQL 注入漏洞的本质不是“字符危险”,而是“语义混淆”——攻击者通过控制查询结构(比如提前闭合字符串、注入新语句)绕过预期逻辑。Go 的 database/sql 包原生支持参数化查询,这才是唯一合规的防御方式。
必须用 db.Query / db.Exec 的占位符传参
所有用户可控输入,必须通过问号占位符(?)传入,由驱动底层完成类型安全绑定。MySQL 驱动用 ?,PostgreSQL 用 $1、$2,SQLite 同样用 ?。硬编码拼接字符串 + 手动转义,等于放弃数据库驱动的安全保障。
示例(MySQL):
// ✅ 正确:参数化查询
rows, err := db.Query("SELECT name FROM users WHERE id = ? AND status = ?", userID, status)
// ❌ 错误:拼接字符串(哪怕加了转义)
query := "SELECT name FROM users WHERE name = '" + escapeSQL(name) + "'"
rows, err := db.Query(query) // 即使 escapeSQL 存在,也无法覆盖所有边界 case
- 占位符不接受列名、表名、ORDER BY 字段等动态结构 —— 这些需白名单校验后硬编码
- 批量插入时,每个值都要独立占位符:
INSERT INTO t VALUES (?, ?), (?, ?),不能只写一个? - 空值(
nil)可直接传入,驱动自动映射为 SQLNULL
哪些场景真需要“转义”?仅限动态标识符
当必须动态构造表名、字段名或 ORDER BY 列时,无法用参数化,此时应严格白名单校验 + 有限转义。Go 标准库不提供通用 SQL 标识符转义函数,但可借助驱动自身方法:
- MySQL 驱动(
github.com/go-sql-driver/mysql)提供mysql.EscapeString(),但它只处理字符串字面量,**不适用于标识符** - 正确做法是:预定义允许的字段名列表,用
map[string]bool检查,匹配失败直接拒绝请求 - 若真要生成带反引号的标识符(如
`user_name`),可用fmt.Sprintf("`%s`", strings.ReplaceAll(name, "`", "``")),但前提是 name 已通过白名单
警惕 ORM 和 Query Builder 的“假参数化”
某些 ORM(如 gorm)或构建器(如 squirrel)表面支持参数化,但若混用字符串插值,依然会破防:
// ❌ gorm 中这样写仍是拼接
db.Where("name = '" + userInput + "'").Find(&users)
// ✅ 必须用结构体、map 或 ? 占位符
db.Where("name = ?", userInput).Find(&users)
db.Where("name = ?", userInput).First(&user)
- 检查你用的库文档,确认其占位符是否最终落到
database/sql的Query原生调用上 - 开启数据库日志(如 MySQL 的
general_log),实际观察生成的 SQL 是否含用户输入原文 - 避免任何
fmt.Sprintf构造完整 SQL 字符串的操作
真正麻烦的从来不是“怎么转义”,而是“哪里不该拼接”。只要守住参数化这条线,99% 的 SQL 注入就不存在。剩下那 1%,往往出在开发者以为自己在用参数化,其实只是看着像。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











