go中sql防注入必须用参数化查询(?或$1),禁用字符串拼接;order by等结构化部分需白名单校验;gorm raw()不自动防护动态拼接;所有用户输入均须统一参数化+白名单。

db.Query 和 db.Exec 必须用 ? 或 $1,不能拼字符串
Go 本身不防注入,防注入靠的是你有没有把用户输入当“值”传进去。只要写 db.Query("SELECT * FROM users WHERE name = '" + name + "'"),就等于把数据库钥匙塞给攻击者。
正确做法是让驱动接管参数绑定:db.Query("SELECT * FROM users WHERE name = ?", name)(MySQL/SQLite)或 db.Query("SELECT * FROM users WHERE name = $1", name)(PostgreSQL)。驱动会把 SQL 模板和参数分开发送,用户输 admin' OR '1'='1 也只会被当做一个字符串值,不会触发语法解析。
- 占位符写错(比如 MySQL 里写
$1)不会报错,但参数会被忽略,查不到数据或意外匹配字面量$1 -
fmt.Sprintf、strconv.Itoa、+拼接一律禁止,哪怕加了单引号也没用 -
db.Query和db.Prepare安全性完全一致,区别只在是否复用执行计划;高频查询用Prepare省点开销,普通场景用Query更直白
ORDER BY / 表名 / 字段名必须走白名单校验
db.Query("SELECT * FROM users ORDER BY ?", "name") 会直接 panic:sql: expected 0 arguments, got 1。SQL 标准规定这些属于查询结构,数据库编译阶段就要确定,没法参数化。
真正该做的,是提前定义合法值并严格比对:
- 排序字段:用
map[string]bool{"created_at": true, "score": true, "name": true},查不到就拒掉 - 排序方向:
if dir != "ASC" && dir != "DESC" { return err },别信strings.ToUpper(dir)后直接拼 - 表名映射:用
switch或常量,比如tenantID == "prod"→ 查users_prod;别写"users_" + tenantID
GORM 的 Raw() 不是免死金牌
db.Raw("SELECT * FROM users WHERE name = ?", name) 是安全的;但 db.Raw(fmt.Sprintf("SELECT * FROM %s", tableName)) 就是裸奔。GORM 不会拦截你塞进去的拼接字符串,它只保证占位符部分被转义。
常见翻车点:db.Raw("UPDATE users SET status = ? WHERE id IN (" + idsStr + ")", status)——idsStr 若来自用户且未校验,idsStr = "1, (SELECT password FROM admins)" 就触发注入。
- 所有动态拼接进
Raw()的部分(表名、字段名、IN列表、WHERE条件片段)都必须白名单校验或预定义枚举 - 批量 ID 查询优先用
db.Where("id IN ?", ids).Update(...),而不是手拼IN子句 - NULL 字段需用
sql.NullString等类型Scan,结构体字段顺序须与SELECT一致,否则可能漏读或 panic
别信“只读接口就安全”这种说法
即使只是 SELECT 查询,攻击者也能通过 UNION SELECT 拖库,或用 AND SLEEP(5) 做盲注探测表结构。所谓“只读账户”只能限制写操作,不能阻止信息泄露。
- 所有用户可控输入,无论接口是 GET 还是 POST,无论返回字段是否敏感,都要统一走参数化+白名单
- 错误日志里别打原始 SQL,更别把
err.Error()直接返回给前端——ERROR: relation "users_xxx" does not exist这类提示等于告诉攻击者表名格式 - 生产环境数据库账号权限必须最小化:删掉
CREATE、DROP、EXECUTE,连SELECT都按表粒度授权
修复不是改一两行代码的事,而是要把所有 SQL 构建路径都拉出来过一遍:哪些地方用了拼接、哪些地方绕过了 ORM、哪些字段名/表名是动态生成的——最容易被忽略的,永远是那些“反正只查一个字段,应该没问题”的临时逻辑。











