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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions