SQL 安全
问题
Go 中如何防御 SQL 注入?使用 ORM 就不用考虑安全了吗?
答案
SQL 注入原理
// ❌ 字符串拼接 → SQL 注入
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userInput)
// 如果 userInput = "'; DROP TABLE users;--"
// 实际执行:SELECT * FROM users WHERE name = ''; DROP TABLE users;--'
防御方式
1. 参数化查询(最关键)
// ✅ database/sql 占位符
db.Query("SELECT * FROM users WHERE name = ?", userInput)
// ✅ sqlx
sqlx.Get(db, &user, "SELECT * FROM users WHERE id = $1", id)
重要
永远不要用 fmt.Sprintf 拼接 SQL。Go 的 database/sql 使用 prepared statement,参数和 SQL 语句分开发送,从根本上杜绝注入。
2. GORM 安全注意事项
GORM 的 Where、Find 等方法默认参数化,但某些写法仍有风险:
// ✅ 安全:参数化
db.Where("name = ?", userInput).Find(&users)
db.Where(&User{Name: userInput}).Find(&users)
// ❌ 危险:字符串拼接传入 GORM
db.Where(fmt.Sprintf("name = '%s'", userInput)).Find(&users)
// ⚠️ 小心 Raw / Exec
db.Raw("SELECT * FROM users WHERE name = ?", userInput) // ✅ 用占位符
db.Raw("SELECT * FROM users WHERE name = '" + input + "'") // ❌
3. 动态列名/表名
列名和表名不能用占位符,需要白名单校验:
// 动态排序字段白名单
var allowedColumns = map[string]bool{
"name": true, "created_at": true, "price": true,
}
func SafeOrderBy(column string) string {
if allowedColumns[column] {
return column
}
return "id" // 默认值
}
db.Order(SafeOrderBy(userInput) + " DESC").Find(&products)
4. LIKE 查询转义
// 转义 LIKE 中的特殊字符 %, _
func EscapeLike(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "%", "\\%")
s = strings.ReplaceAll(s, "_", "\\_")
return s
}
db.Where("name LIKE ?", "%"+EscapeLike(userInput)+"%")
安全检查清单
| 检查项 | 做法 |
|---|---|
| SQL 拼接 | 全部用参数化查询替换 |
| ORM Raw() | 确保用占位符 |
| 动态列名 | 白名单校验 |
| LIKE 查询 | 转义 % 和 _ |
| 错误信息 | 不暴露 SQL 细节给用户 |
| 权限 | 应用用最小权限数据库账号 |
常见面试问题
Q1: database/sql 的占位符为什么能防注入?
答案:底层使用 Prepared Statement,SQL 语句和参数分两次发送给数据库。数据库先编译 SQL 模板,再绑定参数值。参数只会作为数据处理,不会被解析为 SQL 指令。
Q2: GORM 用了 ORM 还需要关注 SQL 注入吗?
答案:需要。GORM 的链式方法内部做了参数化,但以下场景仍有风险:
Raw()/Exec()手写 SQL 时拼接字符串Where()传入拼接好的字符串而非占位符Order()/Group()中直接使用用户输入(这些不支持占位符)