PST-037: Out-of-range numbers and datetimes in the API reach the database layer and answer 500 #766
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#766
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found in the security audit of 2026-10-10 (
SECURITY-AUDIT-2026-10-10.md), run againstmainatc30851bfrom 8 to 10 October 2026, as PST-037. Severity: Low.The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.
src/postulo/api/paging.py(63)Who can do it, and what they need. Holder of an API token (read scope for the list calls, write scope for the POST/PATCH calls). In practice the person's own script or agent, or whoever steals such a token. No privilege is crossed; the only victim is the token's owner. Needs no other account.
How it works.
Reproduced with the Django test client against the real routes. GET /api/v1/{applications,captures,companies,cvs,documents,interviews,letters,listings,listings/choices,offers,reminders}?offset=9223372036854775808 -> IntegrityError('datatype mismatch') (500). GET /api/v1/{applications,listings}?company=<263> and /api/v1/{interviews,offers}?application=<263> -> OverflowError('Python int too large to convert to SQLite INTEGER').
updated_since=0001-01-01T00:00:00+14:00on every list, andsince=on /interviews -> OverflowError('date value out of range'). POST /api/v1/listings or /applications with salary_min/salary_max1e30or1e400-> decimal.InvalidOperation. POST /api/v1/interviews or PATCH /interviews/<pk> with contact_ids [2**63+] -> OverflowError; starts_at/ends_at9999-12-31T23:59:59Z-> OverflowError; POST /applications/<pk>/events, POST /listings/<pk>/events, POST /reminders, PATCH /reminders/<pk> with0001-01-01T00:00:00+14:00-> OverflowError. Persistent variant: POST /api/v1/reminders {"due_at": "9999-12-31T23:59:59Z"} is accepted (201) and stored; with the default TIME_ZONE (Europe/Paris) the web Reminders page (applications:reminder_list) and the calendar agenda view raise OverflowError, and global search (web and /api/v1/search) raises it whenever that reminder is among the hits. The reminder's delete page still renders, so the owner can recover.Impact. Authenticated, self-inflicted unhandled exceptions (500 plus a logged traceback) on a surface whose changelog (#434) says list calls are bounded, and a way to leave stored data that makes several of the owner's pages fail. Siblings show partial hardening: after_id is handled (200 for 2**63), the capture source bounds amounts (plugins/builtin/init.py:134-141
0 < amount < patterns.LARGEST), table number filters were fixed in #499, but the API schemas bound none of these. On PostgreSQL the same values raise DataError (integer/numeric out of range). Not a confidentiality or integrity issue.Evidence.
Verification (verdict: confirmed; severity after verification: Low). Keep it low. Two additions to the candidate. (1) The persistent variant is not limited to reminders. Any timeline or listing-history
occurred_atof year 9999 is accepted with awritetoken, or with the narrowestlistings:bindtoken. It makes the application detail page (applications:detail) or the listing page (jobs:posting_detail) return 500 for the owner, and there is no web route to delete a single event. This contradicts the threat-model sentence that a mail-client token's worst case is 'add entries the person will read'. Root cause: AwareDatetime fields and Decimal fields have no range bound. Suggested bound: reject datetimes outside a sane window (for example 1900 to 2200) and clamp offset, id filters and contact_ids to the signed 64-bit range, or catch OverflowError, IntegrityError and InvalidOperation and return 422. (2) Minor correction to the finding's salary note:1e400as a bare JSON number gives a 422. The string"1e400"givesInvalidOperation. No cross-account or confidentiality impact, and the attacker is only the token's own holder.Suggested remediation. Add bounds in the schemas:
offsetle=2**31-1, id filters and body idsField(ge=1, le=2**31-1), salary Decimals with max_digits=12/decimal_places=2 and the same0 < x < LARGESTrule the capture source uses, datetimes limited to a sane window (for example years 1970-2100) for updated_since, since, due_at, starts_at, ends_at and occurred_at, and cap list lengths (contact_ids, tags). Add a regression test that feeds each parameter its extreme value and asserts 422 rather than 500.Fixed in
d65a01c4aon main.