PST-022: Expired and orphaned sessions are never purged, so held imports (parsed CVs, spreadsheets, vCards) persist and survive account deletion #751
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#751
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-022. 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/core/csv_import.py(csv_import.py:1262-1280);src/postulo/notifications/management/commands/send_due_reminders.py(send_due_reminders.py:176-230);src/postulo/core/views_import.py(63-71)Who can do it, and what they need. Not an active attacker: a privacy and erasure defect. It becomes exploitable for anyone later given a database copy or backup, or access to django_session (an administrator with SQL access, a leaked backup). It also affects people who delete their account.
How it works.
Verified: after upload, expiring the session 30 days back, running tidy_up_if_due() and delete_account(user), the session row still held the spreadsheet.
Impact. Personal data, including other people's names and phone numbers from imported files, outlives its stated retention and the account holder's erasure, and travels into every backup. The data-protection pages and erasure promise do not mention it.
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Files and lines are accurate. The attacker precondition is a database copy or backup taken after the erasure, or an administrator with SQL access; there is no remote attacker. The impact is an erasure and retention defect, not a confidentiality break for live accounts. A suggested fix is to run clear_expired (clearsessions) from the scheduler pass or tidy_up_if_due. A second change is to delete sessions that hold the user's id in delete_account, by scanning the decoded session data for
_auth_user_id, or to use a user-keyed session store. A third is to pop the stash after a short time. The deletion page and the wiki should mention sessions.Suggested remediation. Run clearsessions from the scheduler and tidy_up_if_due. In delete_account(), delete all Session rows whose decoded _auth_user_id equals the user's pk (or store the user id in a column and cascade). Keep imported sheets in an owner-scoped, expiring table instead of the session, and drop them on forget/finish.
Also covered by this issue.
src/postulo/notifications/management/commands/send_due_reminders.py(send_due_reminders.py:176-230); Low). Unbounded growth of django_session and retention of personal data (and third parties' contact data) well past the session's life, compounding the erasure gap above. Only the anonymous-traffic path is clean: anonymous requests were verified to create no session rows.src/postulo/core/views_import.py(63-71); Informational). Personal data held longer and in more places than the import needs (the session copy is forgotten at once, the errand copy is not). Bounded to the owner's own data and to operators with database access; no cross-account path. Informational.Fixed in
e385b1b96on main.