SQL注入是攻击者通过拼接恶意SQL代码篡改查询逻辑的攻击方式;防护核心是禁用字符串拼接,强制使用参数化查询,并对表名、字段名等非值元素实施白名单校验。

直接拼接字符串就是SQL注入的入口
只要把用户输入用 + 或 fmt.Sprintf 塞进 SQL 字符串里,就等于给攻击者开了后门。比如 username := r.FormValue("user") 后写成 "SELECT * FROM users WHERE name = '" + username + "'",一旦输入 ' OR '1'='1,整条查询逻辑就被篡改了。
这种写法在任何数据库驱动下都危险——MySQL、PostgreSQL、SQLite 全部不免疫。别信“我用的是某某驱动所以安全”这种说法。
- 所有用户可控输入(表单、URL 参数、Header、JSON Body)都必须视为不可信
- 哪怕只拼接一个数字 ID,也不能放松:攻击者可传入
1 OR 1=1或1; DROP TABLE users; - 日志里打印拼接后的 SQL 是高危行为,可能泄露构造逻辑或敏感值
用 db.Query 或 db.Exec 的参数化形式最省心
Go 标准库 database/sql 原生支持参数化查询,不需要额外依赖。核心是把 SQL 模板和参数分开传,让驱动负责转义和绑定。
不同数据库占位符不同:?(MySQL)、$1(PostgreSQL)、? 或 :name(SQLite),但用法一致:
// MySQL
rows, err := db.Query("SELECT id, email FROM users WHERE status = ? AND age > ?", "active", 18)
<p>// PostgreSQL
err := db.QueryRow("SELECT name FROM users WHERE id = $1", userID).Scan(&name)
- 参数个数必须严格匹配占位符数量,否则报错
sql: expected X arguments, got Y - 不能对表名、字段名、ORDER BY 子句用占位符——它们不是“值”,数据库不支持参数化
- 批量插入时,多个
?可一次性传入 slice,但需确保长度与占位符一致
动态表名/字段名必须走白名单校验
当业务需要根据请求切换表(如分表)或排序字段(如 ?sort=name),就不能用参数化,但绝不能直接插变量。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
正确做法是预先定义允许的值集合,再做精确匹配:
allowedTables := map[string]bool{"users": true, "products": true, "orders": true}
if !allowedTables[tableParam] {
http.Error(w, "invalid table", http.StatusBadRequest)
return
}
query := "SELECT * FROM " + tableParam + " WHERE deleted = ?"
- 用
map[string]bool或switch判断比正则更安全,避免绕过(如..%2Fetc%2Fpasswd类路径遍历思路) - 禁止用
strings.Contains或前缀检查——攻击者可构造users_xxx; DROP TABLE xxx; - 如果必须支持任意字段排序,至少限制为字母、数字、下划线,且长度 ≤ 32
ORM 不是银弹,GORM 默认安全但配置不当仍会破防
GORM 等 ORM 默认使用参数化查询,db.Where("name = ?", name).Find(&u) 是安全的。但以下操作会退回到拼接模式:
-
db.Where("name = '" + name + "'").Find(&u)—— 显式拼接,完全失效 -
db.Raw("SELECT * FROM users WHERE " + condition).Rows()——condition若来自用户输入,立刻中招 -
db.Table(tableName).Where(...)中的tableName未校验,同上一条风险
用 GORM 时务必确认:所有用户输入都落在 .Where(..., value)、.Select()、.Order() 的参数位置,而非 SQL 字符串内部。
真正容易被忽略的是——预处理语句本身要复用才能发挥最大性能和安全性优势;频繁调用 db.Query 虽安全,但没用到连接池的预编译缓存,而手写 stmt, _ := db.Prepare(...) 又容易忘记 Close() 导致句柄泄漏。平衡点在于:高频固定查询用 Prepare,低频或动态查询用参数化 Query 即可。

















