分页本身无注入风险,但where条件混用字符串拼接与参数化、动态字段/排序/表名未白名单校验、limit/offset未范围限制、select *和scan未显式约束等,均会导致sql注入或dos风险。

Where 条件里混用字符串拼接和参数化,分页照样崩
分页本身不引入注入风险,但分页前的 Where 条件如果写错,整个查询就裸奔。常见错误是:前半段用了参数化,后半段却对用户传入的字段名或操作符做字符串拼接。
比如搜索接口支持按状态、类型、时间范围筛选,有人这么写:
db.Where("status = ?", status).
Where("type IN ('" + strings.Join(types, "','") + "')").
Order("created_at DESC").
Limit(limit).Offset(offset).
Find(&items)
只要 types 是用户可控的切片(如 URL 查询参数 ?type=public&type=admin),攻击者塞个 public', 'admin' UNION SELECT password, email FROM admins -- 就能拖库。
- ✅ 正确做法:所有值都走参数化,
IN子句直接传切片,GORM 自动展开且兼容空切片 - ✅ 若需动态字段(如按
name或email搜索),必须白名单校验:if !validSearchFields[field] { return err } - ⚠️ 注意:MySQL 驱动只认
?,PostgreSQL 只认$1;混用会导致sql: expected 0 arguments, got 1
Order 和 Table 名不能靠 ? 占位符防注入
Order("created_at DESC") 看似安全,但一旦把排序字段或方向变成用户输入(如 ?sort=name&order=desc),直接拼接就是高危操作。
GORM 的 Order 方法不支持参数化绑定——Order("name = ?") 不会把 ? 当占位符处理,而是原样塞进 SQL,等于自毁防火墙。
- ✅ 必须白名单校验字段:
validSortFields := map[string]bool{"name": true, "created_at": true, "updated_at": true} - ✅ 方向也得校验:
if order != "ASC" && order != "DESC" { order = "ASC" } - ✅ 表名同理:
db.Table(tableName)中的tableName绝不能来自用户,必须查白名单或硬编码映射 - ⚠️
Raw()+ 分页更危险:db.Raw("SELECT * FROM " + table + " WHERE ... LIMIT ? OFFSET ?", limit, offset)——表名拼接即失守
Limit 和 Offset 参数化不是万能的,空值和负数要拦住
Limit 和 Offset 接收的是整数,看起来没法注入,但它们影响执行计划和结果集大小,恶意构造可导致 DoS 或数据越界。
- ✅ 显式转换并校验范围:
limit := int64(10),若来自 URL,先转strconv.ParseInt,再限制在合理区间(如 1–100) - ✅
Offset为负数时 GORM 不报错,但 MySQL 会忽略或报错,PostgreSQL 直接拒绝;统一兜底为max(0, offset) - ⚠️ 不要用字符串拼接绕过校验:
db.Limit(10).Offset(0).Where("id > " + strconv.Itoa(id))——id若未校验仍是注入点 - ⚠️ 空切片传给
IN安全,但空字符串传给Like会变成WHERE name LIKE '',可能命中意外数据
Scan 和 Select * 在分页场景下放大静默风险
分页常配合 Scan 或 Select("*") 使用,但这俩不防注入,只管结果映射。一旦前面的 SQL 构造出问题,它们会让错误更隐蔽。
- ✅ 永远显式写字段:
SELECT id, title, status FROM articles,避免因表结构变更导致字段错位 - ✅ 接收用结构体而非
Scan(&id, &title),字段顺序必须严格匹配 SELECT 列序;否则 NULL 值可能 panic 或覆盖错误字段 - ✅ 遇到可能为 NULL 的列(如
deleted_at),必须用sql.NullTime或*string,普通类型遇到 NULL 会直接 panic - ⚠️
Raw().Scan()看似灵活,实则放弃 GORM 所有 ORM 层防护,等于手动开闸放水
分页逻辑越简单越安全。真正难的不是写 Limit 和 Offset,而是守住每一个用户输入进入 SQL 结构的位置——字段名、表名、排序方向、条件操作符,这些地方没有占位符,只能靠白名单和类型约束死。











