PST-038: Oversized integers and non-ASCII digits in ids raise unhandled exceptions (HTTP 500) on many endpoints #767

Closed
opened 2026-10-10 10:56:12 +00:00 by tiagoagueda · 1 comment
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 8 to 10 October 2026, as PST-038. 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.


  • Severity: Low
  • Category: robustness / unhandled-exception
  • Location: src/postulo/core/bulk.py (70-95); src/postulo/jobs/capture_views.py (jobs/capture_views.py:597-601); src/postulo/applications/forms.py (289-293); src/postulo/applications/views.py (applications/views.py:431)
  • Found by: completeness review: coverage, completeness review: tests against claims, ownership review of jobs
  • Related: PST-039 (#768), PST-040 (#769)

Who can do it, and what they need. Any signed-in account. Precondition: a session. Verified with /tmp/.../verify/own-jobs/test_big_ints.py.

How it works.

  1. POST /listings/bulk/ with chosen=999999999999999999999999999999&bulk-action=shortlist (or /jobs/companies/bulk/ with the same chosen, or /jobs/captures/discard/ with selected=<30 digits>).
  2. chosen_ids() accepts any Python int (int(raw)), and CaptureDiscardSelectedView accepts any digit string (value.isdigit()).
  3. The value reaches .filter(pk__in=...); Django 6.1's In lookup has no IntegerFieldOverflow guard (only Exact/Gt/Lt do), so the SQLite adapter raises OverflowError: Python int too large to convert to SQLite INTEGER (PostgreSQL would raise a DataError).
  4. The request ends as a 500 with a traceback in the error log. By contrast the pk= paths (merge with, bind listing, bulk industry, ?company=) answer 404/302/200 because the exact lookup is guarded.

Impact. Repeatable server errors and log noise from any account; no data is read or changed and the response reveals nothing. The project's own words in bulk.py say 'a form that 500s on a stray value is a form somebody can break for fun'.

Evidence.

src/postulo/core/bulk.py:79 `seen.setdefault(int(raw), None)`; :95 `return model.objects.for_user(request.user).filter(pk__in=ids)`. src/postulo/jobs/capture_views.py:597 `chosen = [value for value in request.POST.getlist("selected") if value.isdigit()]`; :600 `.filter(pk__in=chosen, status=CaptureStatus.PENDING)`. Scratch output: `LISTINGS BULK: RAISED OverflowError`, `COMPANY BULK: RAISED OverflowError`, `DISCARD SELECTED: RAISED OverflowError`, while `MERGE WITH HUGE: status 404`, `BIND HUGE: status 404`.

Verification (verdict: confirmed; severity after verification: Low). Two corrections. (1) The claim that PostgreSQL would raise a DataError is unverified here (no PostgreSQL available); with psycopg 3 (the project's declared driver, pyproject.toml:81) an oversized Python int is adapted as numeric and PostgreSQL compares bigint IN (numeric) without error, so the crash is most likely SQLite-specific. SQLite is nevertheless the shipped default, so the finding stands. (2) Add: a legitimate id in the same chosen list is lost too, because the whole request fails (MIXED case), though ATOMIC_REQUESTS means nothing is half-applied. Remediation: in chosen_ids() drop any value outside the pk field's range (e.g. connection.ops.integer_field_range(...) or simply 0 < n < 2**63), apply the same bound in CaptureDiscardSelectedView.post (or route it through bulk.chosen_ids), and add a case to tests/security/test_bulk_actions.py beside test_only_ids_are_only_ids.

Suggested remediation. In chosen_ids() and CaptureDiscardSelectedView drop any id outside the database's signed 64-bit range (or simply any digit string longer than 18-19 characters) before it reaches the queryset, the way as_pk() already swallows non-numbers; add a test that posts a 30-digit id to each bulk endpoint and expects a redirect, not an exception.

Also covered by this issue.

  • str.isdigit() and unguarded date arithmetic let non-ASCII digits and huge numbers crash request paths with 500s (five endpoints plus the spreadsheet import preview) (src/postulo/jobs/capture_views.py (jobs/capture_views.py:597-601); Low). An unhandled exception (HTTP 500, logged with the request line) for the member's own request; for the spreadsheet import a persistent self-inflicted 500 of the import page until the session is reset. No cross-account effect. …
  • Ordinary web forms with multi-select id fields (tags, industries, interview contacts) answer an oversize id with an unhandled OverflowError (500), the same class as the reported bulk-body finding but on different, non-bulk endpoints (src/postulo/applications/forms.py (289-293); Informational). Unhandled exception and logged traceback for a malformed request; on PostgreSQL a DataError (integer out of range). No data exposure and no effect on anyone else. …
  • Oversized integers in ModelMultipleChoiceField forms raise OverflowError (500) on at least ten endpoints beyond the three already listed (src/postulo/applications/views.py (applications/views.py:431); Informational). Unhandled 500 for the member's own request; same class as the confirmed OverflowError finding for CompanyBulkView, ListingBulkView and CaptureDiscardSelectedView, whose list is therefore incomplete. No data crosses accounts.
Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 8 to 10 October 2026, as **PST-038**. 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. --- - **Severity:** Low - **Category:** robustness / unhandled-exception - **Location:** `src/postulo/core/bulk.py` (70-95); `src/postulo/jobs/capture_views.py` (jobs/capture_views.py:597-601); `src/postulo/applications/forms.py` (289-293); `src/postulo/applications/views.py` (applications/views.py:431) - **Found by:** completeness review: coverage, completeness review: tests against claims, ownership review of jobs - **Related:** PST-039 (#768), PST-040 (#769) **Who can do it, and what they need.** Any signed-in account. Precondition: a session. Verified with /tmp/.../verify/own-jobs/test_big_ints.py. **How it works.** 1. POST /listings/bulk/ with chosen=999999999999999999999999999999&bulk-action=shortlist (or /jobs/companies/bulk/ with the same `chosen`, or /jobs/captures/discard/ with selected=&lt;30 digits>). 2. chosen_ids() accepts any Python int (`int(raw)`), and CaptureDiscardSelectedView accepts any digit string (`value.isdigit()`). 3. The value reaches `.filter(pk__in=...)`; Django 6.1's `In` lookup has no IntegerFieldOverflow guard (only Exact/Gt/Lt do), so the SQLite adapter raises `OverflowError: Python int too large to convert to SQLite INTEGER` (PostgreSQL would raise a DataError). 4. The request ends as a 500 with a traceback in the error log. By contrast the `pk=` paths (merge `with`, bind `listing`, bulk `industry`, ?company=) answer 404/302/200 because the exact lookup is guarded. **Impact.** Repeatable server errors and log noise from any account; no data is read or changed and the response reveals nothing. The project's own words in bulk.py say 'a form that 500s on a stray value is a form somebody can break for fun'. **Evidence.** ```text src/postulo/core/bulk.py:79 `seen.setdefault(int(raw), None)`; :95 `return model.objects.for_user(request.user).filter(pk__in=ids)`. src/postulo/jobs/capture_views.py:597 `chosen = [value for value in request.POST.getlist("selected") if value.isdigit()]`; :600 `.filter(pk__in=chosen, status=CaptureStatus.PENDING)`. Scratch output: `LISTINGS BULK: RAISED OverflowError`, `COMPANY BULK: RAISED OverflowError`, `DISCARD SELECTED: RAISED OverflowError`, while `MERGE WITH HUGE: status 404`, `BIND HUGE: status 404`. ``` **Verification** (verdict: confirmed; severity after verification: Low). Two corrections. (1) The claim that PostgreSQL would raise a DataError is unverified here (no PostgreSQL available); with psycopg 3 (the project's declared driver, pyproject.toml:81) an oversized Python int is adapted as numeric and PostgreSQL compares bigint IN (numeric) without error, so the crash is most likely SQLite-specific. SQLite is nevertheless the shipped default, so the finding stands. (2) Add: a legitimate id in the same `chosen` list is lost too, because the whole request fails (MIXED case), though ATOMIC_REQUESTS means nothing is half-applied. Remediation: in `chosen_ids()` drop any value outside the pk field's range (e.g. `connection.ops.integer_field_range(...)` or simply 0 &lt; n &lt; 2**63), apply the same bound in `CaptureDiscardSelectedView.post` (or route it through `bulk.chosen_ids`), and add a case to tests/security/test_bulk_actions.py beside `test_only_ids_are_only_ids`. **Suggested remediation.** In chosen_ids() and CaptureDiscardSelectedView drop any id outside the database's signed 64-bit range (or simply any digit string longer than 18-19 characters) before it reaches the queryset, the way as_pk() already swallows non-numbers; add a test that posts a 30-digit id to each bulk endpoint and expects a redirect, not an exception. **Also covered by this issue.** - **str.isdigit() and unguarded date arithmetic let non-ASCII digits and huge numbers crash request paths with 500s (five endpoints plus the spreadsheet import preview)** (`src/postulo/jobs/capture_views.py` (jobs/capture_views.py:597-601); Low). An unhandled exception (HTTP 500, logged with the request line) for the member's own request; for the spreadsheet import a persistent self-inflicted 500 of the import page until the session is reset. No cross-account effect. … - **Ordinary web forms with multi-select id fields (tags, industries, interview contacts) answer an oversize id with an unhandled OverflowError (500), the same class as the reported bulk-body finding but on different, non-bulk endpoints** (`src/postulo/applications/forms.py` (289-293); Informational). Unhandled exception and logged traceback for a malformed request; on PostgreSQL a DataError (integer out of range). No data exposure and no effect on anyone else. … - **Oversized integers in ModelMultipleChoiceField forms raise OverflowError (500) on at least ten endpoints beyond the three already listed** (`src/postulo/applications/views.py` (applications/views.py:431); Informational). Unhandled 500 for the member's own request; same class as the confirmed OverflowError finding for CompanyBulkView, ListingBulkView and CaptureDiscardSelectedView, whose list is therefore incomplete. No data crosses accounts.
tiagoagueda added this to the 0.6.0 milestone 2026-10-10 10:57:43 +00:00
Author
Owner

Fixed in 8aff05966 on main.

Fixed in 8aff05966 on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#767
No description provided.