PST-079: Kind-file application import writes status, dates and timeline entries directly, bypassing the event log #808
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#808
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-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.
src/postulo/core/kind_files.py(776-830,848-885)Who can do it, and what they need. The account holder, on their own records. Precondition: none.
How it works.
applicationskind file with a row whosestatusis e.g.offer,applied_atandclosed_atset, and aneventslist of arbitrarykind/from_status/to_status/end_reasonvalues (EVENT_FIELDS excludes onlyid,interview_id,created_at)._create_applicationdoesApplication.objects.create(owner=user, posting=posting, **held["fields"])andApplicationEvent.objects.create(application=application, **event)for each event, with only the model form's field validation; nochange_statusruns, 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)
actorpasses 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'sadd_eventand the webEventFormalready do.Fixed in
01f537465on main.