Skip to content

Query-input parsing: NaN limits reach the adapter, date filters skip validation, unchecked casts throughout #47

Description

@cuibonobo

Server design-review finding. The query-parsing helpers in src/routes/records.ts validate almost nothing; malformed input either flows into the adapter as garbage or silently changes the query's meaning. (#33 decides what status malformed input gets — this issue is about the fact that today it isn't detected at all.)

1. limit becomes NaN

GET /records?limit=abcMath.min(parseInt('abc', 10), MAX_QUERY_LIMIT) = Math.min(NaN, 1000) = NaN, assigned to query.limit (records.ts:141) and handed to the adapter's SQL LIMIT. #14's cap fix suggested parseInt(limit, 10) || MAX_LIMIT; the landed version dropped the fallback. The POST body path has the same hole via {"limit": null}? No — but {"limit": NaN} can't arrive via JSON; typeof body.limit === 'number' is safe there except for negatives and non-integers (limit: -5, limit: 2.7), which also pass through unchecked (records.ts:73). Validate: positive integer, else 400.

2. Date filters: the #15 fix missed this file

GET /records?createdBefore=garbage builds new Date('garbage')Invalid Date → NaN comparisons in the adapter (records.ts:117-118, 125-127). This is the exact bug #15 fixed in tokens.ts/types.ts with parseDaterecords.ts's GET path never got the treatment. The POST body path (parseQueryBody, records.ts:50-51, 57-58) uses parseDate but silently maps invalid → undefined, which drops the bound: a query with a malformed createdAt.before runs without it and returns a superset presented as the filtered result — the silent-widening shape haverstack/core#56 bans. Both paths: invalid date → 400.

3. Unchecked casts

parseQueryBody casts every filter field blind — f.tags as string[] accepts "tags": "not-an-array", sort.field accepts any string (records.ts:33-44, 65-71), and parseQueryParams casts ?sort= the same way (records.ts:136-138). Unknown sort fields and mistyped filters should 400, not depend on adapter mercy. Same for the version param: parseInt('1abc', 10) = 1, so GET /records/:id/versions/1abc quietly serves version 1 (records.ts:341, 352) — require an all-digits match.

Work items

Refs #33 (status + body contract), haverstack/core#56 (never silently widen), #32 (fixtures pin GET /records param behavior).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions