PST-040: Other crafted inputs that crash with an unhandled exception #769

Closed
opened 2026-10-10 10:56:13 +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-040. 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: input validation / robustness
  • Location: src/postulo/core/kind_files.py (kind_files.py:405-442); src/postulo/core/cells.py (cells.py:94-102); src/postulo/documents/views.py (1083-1097)
  • Found by: completeness review: coverage, injection and parsers
  • Related: PST-038 (#767), PST-039 (#768)

Who can do it, and what they need. A third party who hands a member a JSON file for one of the four kinds (companies, contacts, listings, applications) or a member who edits an export; the victim is the member who uploads it.

How it works.

  1. Export /export/listings/download/, set listings[0].posted_at to the number 0 (or postulo.exported_at to '2026-01-01T25:61').
  2. POST the file to /export/listings/: answered 302, the rows are pruned by SCHEMAS but values are kept as given and stored in the session.
  3. GET /export/listings/ -> 500 (AttributeError: 'int' object has no attribute 'strip' at kind_files.py:437, or ValueError at :983), and so does every repeat and the confirm POST.
  4. The page that would offer 'Start over' is the one that fails; POST action=forget recovers, as does signing out. Other kinds' pages still work.

Impact. A hostile or merely malformed file disables that import page for the session (a persistent 500, not a one-off). Only the uploader is affected; nothing is written. The candidate file path, fuzzed with 35 leaves and 21 hostile values, does not have this problem.

Evidence.

kind_files.py:437 `form = form_class(data)` after the list/None handling at 413-422 (ints, bools and dicts are passed through). Scratch output: 'KFP upload 302', 'KFP get1 500', 'KFP get2 500', 'KFP other kind page 200', 'KFP forget 302', 'KFP get after forget 200'. Tracebacks: kind_files.py:922 plan -> 916 _verdicts -> 747 _assess_listing -> 735 _clean_listing -> 437 _through_the_form; and plan -> 949 -> 983 _moment.

Verification (verdict: confirmed; severity after verification: Low). Two corrections to the report. First, dicts do not reach the form: file_review.kept() flattens them to [], which becomes a 'this is not text' problem message with no crash. Only ints and bools reach the date field, and only the date fields of the four kinds are affected (posted_at for listings, and the date fields of applications and the other kinds go through the same path). Second, the date crash is more general than listings: exported_at in the header crashes the review page for all four kinds. Recovery is POST action=forget, sign-out and sign-in, or session expiry. Suggested fix: reject non-str, non-None values in _through_the_form() (treat int and bool like list), and wrap _moment() in try/except ValueError.

Suggested remediation. Validate every held value's type when the file is read (text, or a string matching the date shape) and drop or refuse the row there; wrap _moment in try/except; have the review view catch exceptions from plan() and offer the 'forget' form instead of a 500.

Also covered by this issue.

  • Optimistic-concurrency stamp is parsed with parse_datetime() but a well-formed invalid or timezone-less stamp raises instead of being 'treated as not moved' (src/postulo/core/cells.py (cells.py:94-102); Informational). 500 on the member's own cell edit; the docstring promises an unreadable stamp is treated as not moved. Nothing crosses accounts. A far-future aware stamp makes the conflict check pass, which only defeats the member's own stale-edit warning.
  • A cover-letter email subject containing CR/LF ends in an unhandled BadHeaderError (500) after the outbox allowance was spent (src/postulo/documents/views.py (1083-1097); Informational). No injection (Django refuses the header). A confusing 500 and a spent send allowance for the person who typed the subject.
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-040**. 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:** input validation / robustness - **Location:** `src/postulo/core/kind_files.py` (kind_files.py:405-442); `src/postulo/core/cells.py` (cells.py:94-102); `src/postulo/documents/views.py` (1083-1097) - **Found by:** completeness review: coverage, injection and parsers - **Related:** PST-038 (#767), PST-039 (#768) **Who can do it, and what they need.** A third party who hands a member a JSON file for one of the four kinds (companies, contacts, listings, applications) or a member who edits an export; the victim is the member who uploads it. **How it works.** 1. Export /export/listings/download/, set listings[0].posted_at to the number 0 (or postulo.exported_at to '2026-01-01T25:61'). 2. POST the file to /export/listings/: answered 302, the rows are pruned by SCHEMAS but values are kept as given and stored in the session. 3. GET /export/listings/ -> 500 (AttributeError: 'int' object has no attribute 'strip' at kind_files.py:437, or ValueError at :983), and so does every repeat and the confirm POST. 4. The page that would offer 'Start over' is the one that fails; POST action=forget recovers, as does signing out. Other kinds' pages still work. **Impact.** A hostile or merely malformed file disables that import page for the session (a persistent 500, not a one-off). Only the uploader is affected; nothing is written. The candidate file path, fuzzed with 35 leaves and 21 hostile values, does not have this problem. **Evidence.** ```text kind_files.py:437 `form = form_class(data)` after the list/None handling at 413-422 (ints, bools and dicts are passed through). Scratch output: 'KFP upload 302', 'KFP get1 500', 'KFP get2 500', 'KFP other kind page 200', 'KFP forget 302', 'KFP get after forget 200'. Tracebacks: kind_files.py:922 plan -> 916 _verdicts -> 747 _assess_listing -> 735 _clean_listing -> 437 _through_the_form; and plan -> 949 -> 983 _moment. ``` **Verification** (verdict: confirmed; severity after verification: Low). Two corrections to the report. First, dicts do not reach the form: file_review.kept() flattens them to [], which becomes a 'this is not text' problem message with no crash. Only ints and bools reach the date field, and only the date fields of the four kinds are affected (posted_at for listings, and the date fields of applications and the other kinds go through the same path). Second, the date crash is more general than listings: exported_at in the header crashes the review page for all four kinds. Recovery is POST action=forget, sign-out and sign-in, or session expiry. Suggested fix: reject non-str, non-None values in _through_the_form() (treat int and bool like list), and wrap _moment() in try/except ValueError. **Suggested remediation.** Validate every held value's type when the file is read (text, or a string matching the date shape) and drop or refuse the row there; wrap _moment in try/except; have the review view catch exceptions from plan() and offer the 'forget' form instead of a 500. **Also covered by this issue.** - **Optimistic-concurrency stamp is parsed with parse_datetime() but a well-formed invalid or timezone-less stamp raises instead of being 'treated as not moved'** (`src/postulo/core/cells.py` (cells.py:94-102); Informational). 500 on the member's own cell edit; the docstring promises an unreadable stamp is treated as not moved. Nothing crosses accounts. A far-future aware stamp makes the conflict check pass, which only defeats the member's own stale-edit warning. - **A cover-letter email subject containing CR/LF ends in an unhandled BadHeaderError (500) after the outbox allowance was spent** (`src/postulo/documents/views.py` (1083-1097); Informational). No injection (Django refuses the header). A confusing 500 and a spent send allowance for the person who typed the subject.
tiagoagueda added this to the 0.6.0 milestone 2026-10-10 10:57:45 +00:00
Author
Owner

Fixed in ec01219be on main.

Fixed in ec01219be 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#769
No description provided.