Skip to content

[6.x] Fix HTTP 500 on out-of-range front-end pagination page number#14981

Open
mynetx wants to merge 1 commit into
statamic:6.xfrom
mynetx:fix/clamp-paginate-offset-overflow
Open

[6.x] Fix HTTP 500 on out-of-range front-end pagination page number#14981
mynetx wants to merge 1 commit into
statamic:6.xfrom
mynetx:fix/clamp-paginate-offset-overflow

Conversation

@mynetx

@mynetx mynetx commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #14979

Summary

A large but valid ?page= value on any front-end paginated listing returns HTTP 500 instead of an empty page.

Query\Builder::forPage() computes the offset as ($page - 1) * $perPage with no clamp. Once the product overflows PHP_INT_MAX, PHP silently promotes it to a float. offset() only did max(0, $value), which preserves the float, and the Stache driver passes it straight to array_slice(), which throws:

TypeError: array_slice(): Argument #2 ($offset) must be of type int, float given

The overflow is reachable through a real request: Laravel's pagination resolver validates ?page= with filter_var($page, FILTER_VALIDATE_INT), so any value up to PHP_INT_MAX passes through as-is. A crawler following a malformed link, or a scanner probing for this exact class of bug, can trigger it with ?page=9223372036854775807.

Fix

Clamp the overflow in offset() itself rather than in forPage(). Every path that can produce an out-of-range offset routes through it: forPage(), chunk(), and the Stache driver's limitKeys() read $this->offset that offset() sets. One fix covers all three.

Also hardened OrderedQueryBuilder's own offset() override, which had the identical unclamped max(0, $value) shape. It isn't reachable through any first-party call path today (nothing calls ->offset() directly on that wrapper), but it's the same footgun in a sibling Builder implementation and costs nothing to close now.

Test plan

  • Added EntryQueryBuilderTest::it_returns_an_empty_page_when_the_offset_overflows, exercising the real Stache path (forPage(PHP_INT_MAX, 6) → offset-as-float → array_slice). Fails with the TypeError before the fix, passes after.
  • Full EntryQueryBuilderTest, OrderedQueryBuilderTest, and sibling Stache-backed builder suites (Assets/Terms/Users/Forms) pass.
  • Reproduced against a local sandbox via tinker: Entry::query()->forPage(PHP_INT_MAX, 6)->get() threw before the fix, returns an empty collection after.
  • ./vendor/bin/pint clean on changed files.

🤖 Generated with Claude Code

A page number large enough to overflow the offset arithmetic in
Query\Builder::forPage() silently promotes the offset to a float.
offset() preserved that float, so the Stache driver's array_slice()
call threw a TypeError instead of returning an empty page.

Clamp the overflow in offset() itself, since forPage(), chunk(), and
the Stache limitKeys() read all route through it. Also hardened
OrderedQueryBuilder's own offset() override, which had the same
unclamped max(0, $value) shape.

Fixes statamic#14979

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Large ?page= value on a paginated front-end listing returns HTTP 500 (array_slice float TypeError)

1 participant