SEC2-019: With data protection on, deleting a shared contact skips the copy other members are promised and cascades their referee entries #961
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#961
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 on 2026-10-16 in a read-only security review of
postuloat56fc9d531, checked by a second, independent pass.Severity: Medium · Category: Authentication/Authorization · Reference: CWE-285 (Improper Authorization); OWASP A01:2021 (Broken Access Control)
Location:
src/postulo/jobs/views.py:1791-1821(ContactDeleteView.form_valid, thegdpr.is_offeredbranch returns withoutsuper().form_valid(form))src/postulo/jobs/views.py:87-101(KeepsOthersCopies.form_valid, the only caller ofdetach.after_losingon this path)src/postulo/core/gdpr.py:473-510(erase_contactdeletescareer_references(contact)and the contact)src/postulo/resume/models.py:837-840(Reference.contactisCASCADE)Issue
ContactDeleteView.form_validhas two branches. The plain one callssuper().form_valid(form),which is
KeepsOthersCopies.form_valid: it copies the contact to every other member whose ownrows point at it (
detach.after_losing,detach.DEPENDENTS["contact"]: listing events, notes,applications'
contactandreferred_by, appointments, careerReference,ReferenceLetter,interview seats) and only then deletes. The data-protection branch instead calls
gdpr.erase_contact(self.object)and returns a redirect;super().form_validis never reached,so no copy is made.
erase_contactthen deletescareer_references(contact), which is everyowner's
Referencerows sinceReference.contactcascades, and then the contact, whoseSET_NULLrelations unlink every other member's applications, appointments, events and letters.Impact
Where the data-protection module is on for the deleter, the owner of a shared contact (or any
member of an owning group) deletes other members' career referee entries outright and strips
the contact from their applications, appointments and history, without the copy that rule 1 of
the threat model ("whoever loses one keeps a copy of what their own data points at", #896)
promises and that the plain branch provides. It is Medium rather than High because the lost
rows are referee entries and links, not documents, and because it needs the module on.
Fix
Run the copy in both branches: call
detach.after_losing([record], others, check=False)at thetop of
form_valid(before the letters are touched and beforeerase_contact), or restructureso the data-protection branch also goes through
super().form_valid. Done when a test intests/security/test_sharing_sweep.py(ortests/test_gdpr.py) shares a contact with anotherperson who has a career
Referenceand an application naming it, deletes it as the owner withthe module on, and asserts the grantee's
Referencestill exists and points at a copiedcontact, and the grantee's application does too.
Related
#896 (closed) - the copy-on-loss rule this branch skips. SEC2-014 - the same view also deletes other members' reference-letter uploads.