thinkphp分页被绕过因paginate()仅校验页码正整数,不控offset上限,致大offset引发全表扫描;应设page硬上限、用预估总数、禁用裸offset、改游标分页。

ThinkPHP分页查询为什么会被绕过限制做全表扫描
因为 paginate() 默认只校验当前页码是否为正整数,不校验总条数、偏移量或 limit 实际生效范围。攻击者传入极大 page 值(比如 9999999),框架仍会生成 LIMIT 99999990,15 这类语句——MySQL 虽跳过前 99999990 行,但必须先扫描全表统计或遍历索引,I/O 和 CPU 暴涨。
更危险的是,某些场景下开发者手动拼接 limit + offset(如 $query->limit($size)->offset($offset)),而 $offset 完全来自用户输入且无上限,这就等于直接开放全表遍历入口。
- 真实漏洞链:用户传
page=100000&list_rows=100→offset = 9999900→ MySQL 执行SELECT ... LIMIT 9999900,100 - 即使加了索引,大 offset 仍导致索引树深度遍历,性能断崖式下跌
- TP6.0+ 的
paginate()默认启用simple模式时,底层用count()+limit,但 count 本身也可能被拖慢(尤其没主键/没索引的表)
如何用 thinkphp 自带机制硬性截断深度分页
别依赖前端传来的 page 值直接计算 offset,要主动设上限。ThinkPHP 提供了两个关键钩子点:
- 在调用
paginate()前,用input('page/d', 1)强制转整型,并立刻判断:if ($page > 1000) abort(400, 'page too deep'); - 更稳妥的做法是改写分页器:继承
think\Paginator,重写getTotal()方法,在内部对count()结果做缓存或阈值截断(比如超过 50 万就返回 500000) - TP6.3+ 支持
paginate(['page' => $page, 'list_rows' => $size, 'total' => min($estimated, 500000)]),用预估总数代替实时 count,避免慢查询触发
手动 limit/offset 场景下必须做的三件事
如果你没用 paginate(),而是手写 limit + offset(常见于导出、后台批量操作),风险更高,必须人工加固:
-
$offset必须用intval()强转,且严格限制范围:$offset = min(max(0, intval($offset)), 100000) - 永远不要把
$_GET['offset']或input('offset')直接喂给limit()和offset() - 对高频分页接口,加 Redis 缓存分页结果(如
cache('page_'.$key, $data, 300)),并设置固定最大页码(如只允许查前 200 页)
示例:
$offset = input('offset/d', 0);<br>if ($offset > 50000) {<br> throw new HttpException(400, 'offset exceeds limit');<br>}<br>$list = Db::name('user')->limit(20)->offset($offset)->select();
为什么加索引也救不了大 offset 分页
很多人以为“只要字段有索引,limit offset 就快”,这是误解。MySQL 在执行 OFFSET 1000000 时,仍需从索引头开始跳过 100 万行指针,时间复杂度是 O(offset),和是否命中索引无关。唯一可靠解法是改用游标分页(cursor-based pagination)。
- 用上一页最后一条记录的主键值作为下一页起点,例如:
WHERE id > 123456 ORDER BY id LIMIT 20 - ThinkPHP 中可这样写:
Db::name('log')->where('id', '>', $last_id)->limit(20)->select() - 注意:必须有唯一、递增、非空的字段(推荐自增主键或时间戳+唯一ID组合),且排序字段不能有重复值,否则会漏数据
游标分页没法跳转任意页,但能彻底规避深度 offset 导致的全表扫描——这才是真正治本的办法。
实际项目里,分页深度限制和游标分页不是二选一,而是分场景用:列表页用带 page 上限的 paginate(),后台导出或日志追溯这类“只往下翻”的场景,强制切游标分页。最容易被忽略的是——开发时用小数据测不出问题,压测才暴露 offset 性能崩盘,上线前务必用百万级测试表跑一次 page=10000。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










