gorm 的 where 和 first 默认防 sql 注入,因其使用参数化查询;需避免 db.raw() 字符串拼接,动态字段/表名须白名单校验;gin 参数须服务端校验;数据库账号应最小权限。

用 GORM 的 Where 和 First 就能防注入,别手写 SQL 拼接
只要不用 db.Raw() 或字符串拼接构造查询,GORM 默认所有链式调用(如 Where、Joins、Order)都走参数化查询。底层生成的是带占位符的预编译语句,用户输入永远是参数值,不是 SQL 语法的一部分。
常见错误是看到“要查多个字段”就退回到手写 SQL:
- ❌ 错误:用
c.Query("field")拼进db.Raw("SELECT * FROM users WHERE " + field + " = ?") - ✅ 正确:用白名单控制字段名,再传给
Where,比如validFields := map[string]bool{"email": true, "phone": true},校验通过后再db.Where(field+" = ?", value).First(&u) - ⚠️ 注意:
db.Where("status IN ?", statuses)中statuses必须是 slice(如[]int{1,2}),不能是字符串,否则 GORM 会把它当单个参数而非展开列表
db.Raw() 不是不能用,但必须严格用问号占位符
有些场景绕不开原生 SQL,比如复杂聚合、窗口函数、或跨 schema 查询。这时 db.Raw() 是合法出口,但只允许用 ? 占位符传参 —— 且顺序必须和参数列表严格一致。
- ✅ 安全:
db.Raw("SELECT COUNT(*) FROM orders WHERE user_id = ? AND created_at > ?", userID, time.Now().AddDate(0,0,-30)).Scan(&count) - ❌ 危险:
db.Raw("SELECT * FROM " + tableName + " WHERE id = " + idStr)—— 表名、字段名、运算符都不能由用户输入决定 - ? 提示:如果真要动态表名(极少见),必须走白名单校验,比如
allowedTables := []string{"orders_v1", "orders_v2"},再用fmt.Sprintf拼接,绝不能直接插变量
Gin 接收参数时不做校验,等于把注入入口直接敞开
Gin 的 c.PostForm()、c.Query()、c.Param() 只负责取值,不做过滤。你拿到的字符串可能含单引号、分号、注释符 —— 这些在拼接 SQL 时就是炸弹。
- ? 关键点:校验必须在进数据库前完成。比如邮箱字段,用
net/mail.ParseAddress或正则^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$验证格式 - ? 别依赖前端 JS 校验 —— 请求可被 curl / Postman 绕过
- ⚡ 性能提醒:正则校验开销小,但高频接口建议缓存已编译的
*regexp.Regexp实例,避免每次调用都重编译
数据库账号权限最小化,是最后一道物理防线
即使代码层漏掉一处拼接,限制 DB 账号权限也能卡住最危险的操作。GORM 连接用的账号,不该有 DROP、ALTER、CREATE 权限,连 DELETE 都要按业务拆分。
- ✅ 推荐权限组合:
SELECT, INSERT, UPDATE, INDEX—— 够日常 CRUD,删库跑路做不到 - ? MySQL 示例:
GRANT SELECT,INSERT,UPDATE ON myapp.* TO 'gin_app'@'%'; - ⚠️ 容易被忽略:迁移脚本(
AutoMigrate)通常需要CREATE权限,但上线后应切换为只读账号;CI/CD 环境和生产环境 DB 账号必须分离
真正卡住 SQL 注入的,从来不是某一行代码写得“看起来安全”,而是从参数接收、字段白名单、ORM 调用、原生 SQL 占位符、到数据库账号权限,整条链路上没有一个环节松动。少一个,攻击面就多一寸。











