db.prepare()不是万能的,仅防值注入:占位符?只能代入参数值,不能用于表名、列名、order by或limit等结构位置,这些须白名单校验;拼接sql仍危险,必须严格分离模板与数据。

Go 语言操作 MySQL 时,db.Prepare() 不是可选项,而是必须项——只要涉及用户输入,不预处理就等于裸奔。
为什么 db.Query() 和 db.Exec() 不能直接拼接参数
字符串拼接 SQL 是最常见也最危险的写法。比如 "SELECT * FROM users WHERE username = '" + username + "'",一旦 username 是 "admin' OR 1=1 --",整条语句就被注入篡改。
- MySQL 驱动(
go-sql-driver/mysql)本身不做 SQL 解析或过滤,它只负责把传入的字符串原样发给服务端 -
database/sql的Query/Exec方法接收的是完整 SQL 字符串,不区分“结构”和“数据” - 即使加了
strings.ReplaceAll()或正则清理,也无法覆盖所有绕过方式(如编码、多字节字符、注释嵌套等)
db.Prepare() 怎么用才真正防注入
预处理的核心是把 SQL 模板和参数完全分离:模板由数据库编译一次,后续只传值,值永远不参与 SQL 解析。
- 必须用
?占位符,不能用fmt.Sprintf或strconv拼进 SQL 字符串里 - 占位符只能用于值(
WHERE条件、INSERT字段值、ORDER BY ?❌ 不合法),不能用于表名、列名、LIMIT数字(这些需白名单校验或构建时确定) - 推荐复用
*sql.Stmt对象,而不是每次db.Prepare()后立刻Close();连接池会自动管理底层资源
示例:
stmt, err := db.Prepare("INSERT INTO users(username, email) VALUES(?, ?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close() // 注意:不是 defer db.Close()
<p>_, err = stmt.Exec("alice", "alice@example.com")
</p>
预处理不是万能的:哪些地方它压根不生效
db.Prepare() 只解决“值注入”,对结构层面的注入完全无感。
-
SELECT * FROM ?—— 表名不能用?,必须提前确认合法性(比如从固定枚举中取值) -
ORDER BY ?—— 列名同理,可用map[string]bool{"created_at": true, "username": true}白名单校验 -
LIMIT ?虽然语法允许,但若?来自用户且未校验范围,可能引发 DOS 或数据泄露(如LIMIT 1000000) - 动态
WHERE子句(如多个可选条件)需手动生成 SQL 模板,再统一预处理,不能靠if拼接后直接执行
真正难的从来不是写对一行 stmt, _ := db.Prepare(...),而是判断哪里该用它、哪里不能用、以及模板之外的部分怎么安全兜底。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











