PST-079: Kind-file application import writes status, dates and timeline entries directly, bypassing the event log #808

Closed
opened 2026-10-10 10:56:48 +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-079. Severity: Informational.

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: Informational
  • Category: business-logic / event-log-integrity
  • Location: src/postulo/core/kind_files.py (776-830,848-885)
  • Found by: business logic and imports

Who can do it, and what they need. The account holder, on their own records. Precondition: none.

How it works.

  1. Upload an applications kind file with a row whose status is e.g. offer, applied_at and closed_at set, and an events list of arbitrary kind/from_status/to_status/end_reason values (EVENT_FIELDS excludes only id, interview_id, created_at).
  2. Confirm the review page.
  3. _create_application does Application.objects.create(owner=user, posting=posting, **held["fields"]) and ApplicationEvent.objects.create(application=application, **event) for each event, with only the model form's field validation; no change_status runs, so an application can be at Offer with no status-change entry, or carry status-change entries that contradict its status.

Impact. No cross-account effect and no privilege change: the person is editing their own history. It contradicts the project's stated rule that the event log is the truth and status changes go through change_status (CLAUDE.md, applications/views.py docstrings) and makes the analytics (reports.py, which reads events) trustable only as far as the person's file is. Recorded as informational.

Verification (verdict: confirmed; severity after verification: Informational). Attacker and effect as claimed (the account holder, on their own records), with two additions: (1) the file can write kinds in SYSTEM_EVENT_KINDS (status_change, interview_scheduled, interview_cancelled, interview_no_show), which the API and the form deliberately refuse, so this is the one door that lets the log contradict the field; (2) actor passes through, so an imported entry can be signed with any agent's name (e.g. a mail-client token's), weakening the provenance the threat model relies on for agent-written entries, still only in the owner's own log. Also closed_at/applied_at are written as given, so closed_at may precede applied_at and may disagree with the status (change_status maintains both). …

Suggested remediation. Create the application as draft and replay the file's status through change_status (as the CSV importer does), and refuse or re-label system event kinds (SYSTEM_EVENT_KINDS) coming from a file, as the API's add_event and the web EventForm already do.

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-079**. Severity: **Informational**. 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:** Informational - **Category:** business-logic / event-log-integrity - **Location:** `src/postulo/core/kind_files.py` (776-830,848-885) - **Found by:** business logic and imports **Who can do it, and what they need.** The account holder, on their own records. Precondition: none. **How it works.** 1. Upload an `applications` kind file with a row whose `status` is e.g. `offer`, `applied_at` and `closed_at` set, and an `events` list of arbitrary `kind`/`from_status`/`to_status`/`end_reason` values (EVENT_FIELDS excludes only `id`, `interview_id`, `created_at`). 2. Confirm the review page. 3. `_create_application` does `Application.objects.create(owner=user, posting=posting, **held["fields"])` and `ApplicationEvent.objects.create(application=application, **event)` for each event, with only the model form's field validation; no `change_status` runs, so an application can be at *Offer* with no status-change entry, or carry status-change entries that contradict its status. **Impact.** No cross-account effect and no privilege change: the person is editing their own history. It contradicts the project's stated rule that the event log is the truth and status changes go through `change_status` (CLAUDE.md, applications/views.py docstrings) and makes the analytics (`reports.py`, which reads events) trustable only as far as the person's file is. Recorded as informational. **Verification** (verdict: confirmed; severity after verification: Informational). Attacker and effect as claimed (the account holder, on their own records), with two additions: (1) the file can write kinds in SYSTEM_EVENT_KINDS (status_change, interview_scheduled, interview_cancelled, interview_no_show), which the API and the form deliberately refuse, so this is the one door that lets the log contradict the field; (2) `actor` passes through, so an imported entry can be signed with any agent's name (e.g. a mail-client token's), weakening the provenance the threat model relies on for agent-written entries, still only in the owner's own log. Also closed_at/applied_at are written as given, so closed_at may precede applied_at and may disagree with the status (change_status maintains both). … **Suggested remediation.** Create the application as draft and replay the file's status through `change_status` (as the CSV importer does), and refuse or re-label system event kinds (`SYSTEM_EVENT_KINDS`) coming from a file, as the API's `add_event` and the web `EventForm` already do.
tiagoagueda added this to the 1.0.0 milestone 2026-10-10 10:58:12 +00:00
Author
Owner

Fixed in 01f537465 on main.

Fixed in 01f537465 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#808
No description provided.