Skip to content

Fix: rescue malformed page param in Pagy::Keyset and KeynavJsPaginator#907

Merged
ddnexus merged 4 commits into
ddnexus:devfrom
7a6163:fix/keyset-rescue-malformed-page
May 24, 2026
Merged

Fix: rescue malformed page param in Pagy::Keyset and KeynavJsPaginator#907
ddnexus merged 4 commits into
ddnexus:devfrom
7a6163:fix/keyset-rescue-malformed-page

Conversation

@7a6163
Copy link
Copy Markdown
Contributor

@7a6163 7a6163 commented May 24, 2026

Summary

Two sibling locations call JSON.parse(B64.urlsafe_decode(page)) on an attacker-controlled request param without rescuing exceptions. Any malformed ?page= value (invalid base64, or valid base64 of non-JSON) raises uncaught and propagates as a 500:

GET /items?page=garbage    # -> 500 ArgumentError: invalid base64
GET /items?page=aGVsbG8    # -> 500 JSON::ParserError: unexpected character

Affected sites:

  • gem/lib/pagy/classes/keyset/keyset.rbPagy::Keyset#assign_page
  • gem/lib/pagy/toolbox/paginators/keynav_js.rbKeynavJsPaginator.paginate

This patch rescues both ArgumentError and JSON::ParserError at both call sites, matching Pagy::Request#resolve_page's existing silent-fallback-to-page-1 convention for malformed integer page params.

Why

This is not a security issue — an attacker doesn't gain data, access, or availability, and Ruby/puma handles 500 responses as fast as 200s so there's no DoS amplification. It's filed as a robustness fix.

But the small change improves operational behavior:

  • Cleans up unhandled exceptions in Sentry/Datadog from web scanners and curious users
  • Stops triggering 5xx-rate alerts on benign garbage input
  • Lets clients distinguish "bad request" from "server is broken" for retry logic

Changes

  • gem/lib/pagy/classes/keyset/keyset.rb — method-level rescue JSON::ParserError, ArgumentError that nils @page
  • gem/lib/pagy/toolbox/paginators/keynav_js.rbbegin/rescue around the inline JSON.parse so options[:page] stays unset on parse failure
  • test/unit/pagy/classes/keyset/keyset_test.rb — 2 new tests covering both exception paths against both Pet (ActiveRecord) and PetSequel (Sequel) via the existing [Pet, PetSequel].each loop
  • test/unit/pagy/toolbox/paginators/keynav_test.rb — 2 new tests covering both exception paths via MockApp

Test plan

  • bundle exec ruby -Ilib:test -Igem/lib -e 'ARGV.each{|f| require "./#{f}"}' test/unit/pagy/classes/keyset/keyset_test.rb — 43 tests, 92 assertions pass
  • bundle exec ruby -Ilib:test -Igem/lib -e 'ARGV.each{|f| require "./#{f}"}' test/unit/pagy/toolbox/paginators/keynav_test.rb — 6 tests, 14 assertions pass
  • bundle exec thor test:api:all — current + next API both green, 0 failures, 0 regressions
  • bundle exec rubocop on all 4 changed files — 0 offenses
  • Coverage neutral vs current master (master baseline on bundle exec thor test:api:coverage is currently 95.46% line / 98.70% branch; this patch goes to 95.47% / 98.71%, net positive)

Notes for reviewer

  • @page = nil on rescue in Keyset#assign_page is intentional — downstream attr_reader :page then returns nil, matching the "no page param given" code path. @prior_cutoff stays unset (since the JSON.parse line that would set it didn't complete), so fetch_records correctly skips the apply_where(...) branch.
  • In KeynavJsPaginator.paginate, leaving options[:page] unset on rescue means Keyset::Keynav#assign_page (line 49) takes the @page = @last = 1 branch — same first-page behavior as if no page param had been sent.
  • Branched off master per CONTRIBUTING.md.

ddnexus and others added 3 commits May 13, 2026 09:14
Two sibling locations call `JSON.parse(B64.urlsafe_decode(page))` on
an attacker-controlled request param without rescue. Any malformed
?page= value (invalid base64, or valid base64 of non-JSON) raises
uncaught and propagates as a 500.

This patch rescues both `ArgumentError` and `JSON::ParserError` at
both call sites, matching `Pagy::Request#resolve_page`'s existing
silent-fallback-to-page-1 convention for malformed integer page params.

Before: GET /items?page=garbage  -> 500 (JSON::ParserError or ArgumentError)
After:  GET /items?page=garbage  -> 200 first page

Sites fixed:
- gem/lib/pagy/classes/keyset/keyset.rb   (Pagy::Keyset)
- gem/lib/pagy/toolbox/paginators/keynav_js.rb  (KeynavJsPaginator)

Tests cover both ArgumentError and JSON::ParserError paths against
both Pet (ActiveRecord) and PetSequel (Sequel) models for Keyset,
and Pet (ActiveRecord) for KeynavJsPaginator.
@7a6163 7a6163 force-pushed the fix/keyset-rescue-malformed-page branch from 750465a to 669e0d7 Compare May 24, 2026 06:52
@7a6163 7a6163 changed the title Fix: rescue malformed page param in Pagy::Keyset Fix: rescue malformed page param in Pagy::Keyset and KeynavJsPaginator May 24, 2026
@ddnexus
Copy link
Copy Markdown
Owner

ddnexus commented May 24, 2026

@7a6163 Thank you! This is one of the best contribution Pagy ever received! 🙌
It shows a deep understanding of its code, intentions and policies. Great description and explanation.

While reviewing your changes, I noticed that the my original Keyset#assign_page had redundant code, so I simplified it and extracted the Keyset.decode function to reduce duplication.

Thank you.

@ddnexus ddnexus changed the base branch from master to dev May 24, 2026 10:47
@7a6163
Copy link
Copy Markdown
Contributor Author

7a6163 commented May 24, 2026

Thanks @ddnexus — much cleaner. Tests green locally.

@ddnexus ddnexus merged commit 9161301 into ddnexus:dev May 24, 2026
6 checks passed
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.

3 participants