分页参数必须手动校验,gorm 不拦截非法值;page

分页参数必须手动校验,GORM 不做任何拦截
GORM 对 page 和 size 完全放行,传 page=-999、size=1000000 会直接生成 OFFSET -999 LIMIT 1000000,MySQL 报错或拖垮连接池都是分分钟的事。
校验逻辑不能依赖前端、不能只做类型转换,得在解析后立刻处理:
-
page小于 1 时强制设为 1(别返回错误,用户点错“第 0 页”很常见) -
size必须硬限制范围,比如 1–100;超出就截断(如设成 20),而不是拒掉请求 - 用
r.URL.Query().Get("page")拿值,空字符串先判空再调strconv.Atoi,避免 panic - 别用
r.ParseForm(),它会把所有参数塞进r.Form,但空值、重复键、编码异常都更难控制
ORDER BY 是分页稳定的前提,不是可选项
没 ORDER BY 的 OFFSET/LIMIT 在 PostgreSQL 或 MySQL 下都不保证结果一致性——并发写入时漏数据、重复返回是常态,这不是 GORM 的 bug,是 SQL 语义决定的。
排序字段必须带索引,且要能提供确定性顺序:
- 优先用主键:
.Order("id ASC")或时间戳+主键组合:.Order("created_at DESC, id DESC") - 避免单独用
.Order("created_at DESC"):多条记录时间相同时,数据库返回顺序不可控 - 禁止
.Order("RAND()"):性能差、无法翻页、不满足业务语义
Count 查询必须隔离上下文,否则永远 ≤ size
db.Where("status = ?", status).Limit(10).Offset(20).Count(&total) 得到的 total 是加了 LIMIT 后的结果数,永远 ≤ 10。这是 GORM 分页最常被踩的坑。
正确做法有两种:
- 用新会话隔离:
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where("status = ?", status).Count(&total) - 复杂 JOIN 场景下,手写子查询更可靠:
db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u JOIN profiles p ON u.id = p.user_id WHERE u.status = ?) AS t", status).Scan(&total) - 如果业务允许(比如 Feed 流),可跳过总数,只查
size + 1条,用len(results) > size判断是否有下一页
IN 子句和动态表名是注入高发区,占位符不万能
IN 子句本身支持参数化,但写法有严格要求:必须用 db.Where("id IN ?", []uint{1,2,3}),GORM 会自动展开为 IN (1,2,3);而 db.Raw("id IN (" + idsStr + ")") 或 db.Where("id IN (" + idsStr + ")") 都是裸奔。
更危险的是表名、字段名、排序字段这些「非值」内容,占位符根本无效:
- 表名拼接必须白名单校验:
validTables := map[string]bool{"users": true, "orders": true},不在其中就 panic - 排序字段不能由用户直接传,得映射成固定选项:
map[string]string{"created": "created_at DESC", "name": "name ASC"} - 别信“过滤单引号/分号”,PostgreSQL 可用
$1、Unicode 变体绕过,黑名单策略早已失效
真正容易被忽略的,是那些看起来“只是业务字段”的输入——比如 status、category,它们不参与 SQL 构造,但若没做白名单校验,就可能被用来绕过权限控制或触发条件逻辑漏洞。防注入不只是防 ' OR 1=1 --,更是防整个查询语义被篡改。











