Skip to content

fix: close pre-GA decode-hardening gaps - #893

Merged
tada5hi merged 4 commits into
masterfrom
fix/pre-ga-decode-hardening
Aug 10, 2026
Merged

fix: close pre-GA decode-hardening gaps#893
tada5hi merged 4 commits into
masterfrom
fix/pre-ga-decode-hardening

Conversation

@tada5hi

@tada5hi tada5hi commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Closes the pre-GA gaps surfaced while validating the FLAME hub against 2.0.0-beta.19 (all hub suites green on the beta — these are the residual findings).

Pagination joins the prototype-member key guard

Pagination was the one parameter the #883 hardening did not cover: it never expands or groups its keys, so a hostile page key was silently ignored rather than rejected. BaseParser gains assertSafeObjectKeys() (depth-bounded like expandObject), and SimplePaginationParser.parse runs it explicitly — a pagination key with a __proto__/constructor/prototype segment now raises the same typed ParseError as the other four parameters, in both dialects.

BREAKING: such keys previously parsed (ignored); they now throw. Legitimate clients are unaffected.

url.search tolerance on decode

codec.decode('?filter[name]=a') decoded to an empty query — qs reads ?filter as the first key name, so every parameter was silently lost. A single leading ? is now stripped before parsing (the tolerance URLSearchParams applies), shared by the facade's stamp dispatch, the dialect decoders and every per-parameter helper — so dialect detection and decoding see the same payload. ??filter stays unrecognized: only one ? is stripped.

throwOnFailure doc on the expression exception

The dialect asymmetry on disallowed filter keys (simple prunes per the schema's drop-vs-throw policy; expression always throws) is deliberate and documented in the guide — but FiltersParseOptions.throwOnFailure itself did not say the expression parser ignores it. The option's declaration now states the exception and its rationale, so a caller passing throwOnFailure: false to the expression parser is not left wondering why nothing changed.

Verification

  • Full monorepo npm run build + npm run test green on this branch (10 test projects).
  • New pins: hostile pagination keys across both dialects incl. the N:-prefixed and nested own-property forms, no-pollution negative controls, ?-tolerance on stamped/unstamped payloads and the decodePagination helper, single-?-only semantics.
  • Downstream: FLAME hub runs 2.0.0-beta.19 with its own capability suite pinning the guard, the 400 mapping, and the codec round trip (PrivateAIM/hub@116dbdb50).

Summary by CodeRabbit

  • Bug Fixes

    • URL query parsing now consistently handles a single leading ? across decoding, filtering, sorting, relationships, and pagination.
    • Pagination input rejects unsafe object keys and deeply nested paths, preventing prototype pollution while preserving valid limit and offset parsing.
  • Security

    • Added recursive validation for parsed object keys to block hostile property names and excessive traversal depth.
  • Documentation

    • Clarified failure-handling behavior for filter expressions.
  • Tests

    • Added coverage for leading-question-mark handling, stamped payloads, pagination, and prototype-pollution protection.

Pagination was the one parameter the #883 hardening did not cover: it
never expands or groups its keys, so a hostile page key was silently
ignored instead of rejected. The guard contract is now uniform — a
pagination key whose path addresses an inherited prototype member
raises the same typed ParseError as the other four parameters, in both
dialects (the expression pagination parser subclasses the simple one).
Nesting is bounded by the shared traversal depth, mirroring
expandObject.

BREAKING CHANGE: a pagination key containing a __proto__, constructor
or prototype segment now raises a typed ParseError instead of being
ignored.
qs treats a leading '?' as part of the first key name, so a caller
passing url.search verbatim decoded to an empty query — every
parameter silently lost ('?filter' is not a known wire key). A single
leading '?' is now stripped before parsing, the same tolerance
URLSearchParams applies. The strip is shared by the codec facade's
stamp dispatch, the dialect decoders' main path and every per-parameter
decode helper, so dialect detection and decoding see the same payload.
FiltersParseOptions.throwOnFailure is honored by the simple and mongo
dialects but deliberately ignored by the expression dialect, which
always throws on a key resolution failure — an expression cannot be
partially reinterpreted safely, since pruning a leaf inside or(...)
changes the compound's meaning rather than narrowing it. The guide
documents this in several places; the option's own declaration now
says it too, so a caller passing throwOnFailure: false to the
expression parser is not left wondering why nothing changed.
Copilot AI lite review requested due to automatic review settings August 10, 2026 11:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR centralizes URL query-string parsing through parseQueryString. It adds leading-question-mark coverage and validates pagination object keys against unsafe and excessively deep paths.

Changes

Codec and parser hardening

Layer / File(s) Summary
Shared URL query parsing
packages/codec-url/src/utils/parse.ts, packages/codec-url/src/utils/index.ts, packages/codec-url/src/module.ts, packages/codec-url/src/decoder/module.ts, packages/codec-url/test/unit/*
parseQueryString removes one leading ? before parsing. URL codec preparation and field, filter, pagination, relation, and sort decoders use the utility. Tests cover unstamped, stamped, pagination, and repeated-? inputs.
Pagination key validation
packages/core/src/parser/base.ts, packages/parser-simple/src/parameter/pagination/module.ts, packages/parser-simple/test/unit/parser/prototype-pollution.spec.ts, packages/core/src/parser/parameter/filters/types.ts
assertSafeObjectKeys rejects unsafe keys and paths beyond the traversal-depth limit. Pagination parsing applies the validation before reading fields. Tests cover hostile keys and valid pagination input. Filter failure behavior is documented.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the pull request's main purpose: closing decode-hardening gaps before GA.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pre-ga-decode-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/codec-url/src/utils/parse.ts`:
- Around line 16-17: Update parseQueryString to validate decoded query keys
before passing them to parse, rejecting constructor and prototype path segments
(and __proto__ consistently) using the typed ParseError.inputInvalid() contract;
ensure nested bracket keys are checked rather than relying only on top-level
Object.keys validation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 857739ef-0f98-47b4-b48e-481e6547b0d9

📥 Commits

Reviewing files that changed from the base of the PR and between 102be34 and ee0fe70.

📒 Files selected for processing (10)
  • packages/codec-url/src/decoder/module.ts
  • packages/codec-url/src/module.ts
  • packages/codec-url/src/utils/index.ts
  • packages/codec-url/src/utils/parse.ts
  • packages/codec-url/test/unit/codec.spec.ts
  • packages/codec-url/test/unit/simple-pagination.spec.ts
  • packages/core/src/parser/base.ts
  • packages/core/src/parser/parameter/filters/types.ts
  • packages/parser-simple/src/parameter/pagination/module.ts
  • packages/parser-simple/test/unit/parser/prototype-pollution.spec.ts

Comment thread packages/codec-url/src/utils/parse.ts
@tada5hi

tada5hi commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai pause

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews paused.

qs itself drops __proto__/constructor bracket structures (they are
Object.prototype members), so those never reach the parser; prototype
under a safe parent is the one form qs passes through, and the
parser-level guard rejects it. Two pins hold both halves of that
boundary on the wire path.
@tada5hi
tada5hi merged commit 883ac09 into master Aug 10, 2026
4 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 10, 2026
tada5hi added a commit to authup/authup that referenced this pull request Aug 11, 2026
…-beta.20) (#3425)

* feat: bump rapiq to 2.0.0-beta.20 and answer decode failures with 400

The release ships schema index declarations (tada5hi/rapiq#895) and
closes the pre-GA decode-hardening gaps (tada5hi/rapiq#893): a
pagination key carrying a prototype segment now raises a typed
ParseError instead of being ignored, and routup's qs passes
'prototype' segments through, so the throw is reachable over HTTP.

sanitizeError maps rapiq ParseError/CodecError onto BAD_REQUEST: a
decode failure is client wire input, not a server fault. The
expression dialect has always thrown on disallowed keys, so those
requests answered 500 before this. Pinned over HTTP for both paths.

* feat: declare schema indexes and back the query surface with entity indexes

Every registered schema declares its `indexes` and opts into the
rapiq 2.0.0-beta.20 indexed filters (anchor mode) and sort policies,
under one invariant: every allowed filter/sort key LEADS a declared
index backed by a real database structure, so enforcement never
rejects a query the allow-lists permit. The only narrowing is a
multi-key sort without a matching composite prefix, which drops
whole-parameter (no UI surface sends one).

Migration 1786436332251-QueryIndexes backs the declarations: 126 new
indexes covering the query vocabulary, every remaining FK scalar
column (junction *_realm_id and policy_id columns,
client.access_policy_id, the EA and identity-provider mapping tables;
a Postgres-only gap since MySQL keeps implicit FK indexes), the
login-throttle composite (actor_name, request_ip_address, created_at)
replacing the redundant actor_name single, session_tokens.parent_id
(grace-window lookup), and the reset_hash/activate_hash token lookups
(sole selective predicate on unauthenticated endpoints). It also
drops the three orphaned legacy tables (auth_authorization_codes,
auth_refresh_tokens, auth_identity_provider_roles); down() recreates
them exactly as the pre-existing chain leaves them.

The mysql down() wraps the generated statements with hand-authored
FK drop/re-add phases: MySQL silently drops a constraint's implicit
index once a created index can serve it, so the plain DROP INDEX
would fail with ER 1553 on 50 constraints.

assertSchemaIndexesMatchEntity joins the boot-time validation pass
(a declared sequence must be a leftmost prefix of a real
PK/unique/index; tada5hi/rapiq#898 tracks upstreaming it).

Verified on both dialects: run, revert x16, re-run, schema-drift
gate, populated round-trip.

* docs: record the index declarations and the mysql FK-index down trap

Query IR flow gains the index-declaration rules (adding a filterable
or sortable key now requires a backing entity index plus migration);
conventions and the typeorm reference record that MySQL auto-drops a
foreign key's implicit index when a created index can serve the
constraint, breaking generated down() paths, and the wrap pattern
that restores the exact pre-migration state.

* build: bump rapiq to the stable 2.0.0 release

A clean re-tag of 2.0.0-beta.20 - no content changes; the full suite
re-verified against the resolved release artifacts.
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.

2 participants