Skip to content

fix(das): validate cursor payload fields instead of returning 500#228

Open
rsnetworkinginc wants to merge 1 commit into
entrius:testfrom
rsnetworkinginc:fix-cursor-payload-validation
Open

fix(das): validate cursor payload fields instead of returning 500#228
rsnetworkinginc wants to merge 1 commit into
entrius:testfrom
rsnetworkinginc:fix-cursor-payload-validation

Conversation

@rsnetworkinginc

Copy link
Copy Markdown

PR body — rsnetworkinginc — entrius/das-github-mirror

Branch: fix-cursor-payload-validation (in WSL Ubuntu-22.04 clone /root/dgm, commit 2c376e7, based on origin/test @ a188d3b)
Base: test
Title: fix(miners): reject unparseable cursor payloads with 400 instead of a Postgres 500


Bug

decodeCursor (pagination for /api/v1/miners/:githubId/pulls|issues) checks that the decoded payload's fields have the right types, but never that created_at is actually a timestamp or that number is an integer. Both values are bound to casts in the keyset WHERE clause:

AND (p.created_at, LOWER(p.repo_full_name), p.pr_number) < ($3::timestamptz, $4, $5::int)

So a cursor that is valid base64url JSON with correctly-typed fields sails through validation and blows up inside Postgres, returning 500 Internal Server Error for malformed client input:

# {"created_at":"banana","repo_full_name":"acme/repo","number":1}
GET /api/v1/miners/1/pulls?limit=50&cursor=eyJjcmVhdGVkX2F0IjoiYmFuYW5hIiwicmVwb19mdWxsX25hbWUiOiJhY21lL3JlcG8iLCJudW1iZXIiOjF9
  -> 500 (invalid input syntax for type timestamp with time zone: "banana")

# {"created_at":"2026-06-01T00:00:00.000Z","repo_full_name":"acme/repo","number":1.5}
GET /api/v1/miners/1/pulls?limit=50&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wNi0wMVQwMDowMDowMC4wMDBaIiwicmVwb19mdWxsX25hbWUiOiJhY21lL3JlcG8iLCJudW1iZXIiOjEuNX0
  -> 500 (invalid input syntax for type integer: "1.5")

Base64url-decoded JSON that fails the shape check already gets a clean 400 ("cursor is malformed") — these two payloads are the gap. Since 5xx is conventionally retryable, a client holding a corrupted/truncated cursor will keep re-sending a request that can never succeed instead of restarting pagination.

Fix

Tighten the malformed-cursor check in decodeCursor:

  • created_at must satisfy !Number.isNaN(Date.parse(...));
  • number must satisfy Number.isInteger(...) (subsumes the previous isFinite check).

Both reuse the existing BadRequestException("cursor is malformed"). No false rejections are possible: encodeCursor only ever emits toISOString() timestamps and integer pr_number/issue_number values, so every server-issued cursor still decodes — only tampered or corrupted cursors are affected.

Tests

New pagination.spec.ts (unit):

  • encodeCursordecodeCursor round-trip (incl. repo lowercasing);
  • rejection of non-base64url input, missing/mistyped fields (existing behavior, now pinned);
  • rejection of unparseable created_at and non-integer number (the two new cases);
  • parsePaginationQuery propagates cursor validation, still returns null when neither limit nor cursor is given, and accepts a valid limit+cursor pair.

Written red-first against the current test tip: the 3 new-validation cases fail before the fix, all 8 pass after.

Local checks

  • npm test — 33 passed (4 suites)
  • npm run lint — clean
  • npm run format:check — clean
  • npm run build — clean

Note on cap: rsnetworkinginc already has #221 open (fetch.processor ISSUE_CLOSURE guard). Cap is 2 per the triage bot, so this is the second and last slot — do not open anything else under this account until one of the two resolves.

… Postgres 500

decodeCursor type-checks the decoded payload but never checks that
created_at is actually a timestamp or that number is an integer. Both
values are bound to casts in the keyset pagination WHERE clause
((created_at, repo, number) < ($::timestamptz, $, $::int)), so a cursor
like {"created_at":"banana","repo_full_name":"a/b","number":1} — valid
base64url JSON with the right field types — sails through validation and
blows up in Postgres, returning 500 Internal Server Error for what is a
malformed client input. Clients that retry on 5xx will re-send a cursor
that can never work.

Tighten the malformed-cursor check: created_at must parse as a date and
number must be an integer. encodeCursor only ever emits toISOString()
timestamps and integer pr/issue numbers, so no server-issued cursor is
affected; only tampered or corrupted cursors now get the 400 they always
should have.

Adds unit tests for decodeCursor/parsePaginationQuery covering the
round-trip, both new rejection cases, and existing behavior.
@xiao-xiao-mao xiao-xiao-mao Bot added the bug Something isn't working label Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant