SEC2-016: Delete-confirmation facts count other members' private rows and are drawn for every reader #958
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#958
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: Data Exposure · Reference: CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor); OWASP A01:2021 (Broken Access Control)
Location:
src/postulo/jobs/views.py:1394-1471(CompanyDeleteView.get_context_data),:1948-2034(PostingDeleteView.get_context_data),:1769-1789(ContactDeleteView.get_context_data)src/postulo/jobs/views.py:870, 1921(CompanyDetailViewandPostingDetailViewcalldeletions.facts_for(...)for every reader,access = READ)src/postulo/templates/jobs/company_detail.html:60-66, 552-553andsrc/postulo/templates/jobs/posting_detail.html:200-201(the Delete button and the<c-delete-dialog :facts="deletion">are drawn with nomanagesgate)src/postulo/core/gdpr.py:179-201(references) and:473-510(erase_contact, the same unnarrowed counts in its summary)Issue
The consequences a deletion lists are computed over the whole record, not over the reader's
own rows:
Likewise
PostingDeleteViewcountsself.object.applications.all(),self.object.events.all()(every person's history entries),
self.object.captures.all()and theRenderedDocumentrowsof everybody's applications, worded as "documents you sent". On a shared company or
listing these include applications, appointments, contacts, references, captures and history
entries that belong to other members: rows the threat model says are "theirs whatever is
shared", and that every other query on these pages narrows (
own_applications,Contact.objects.visible_to,events ... owner=user). The erasure summary of a contact underthe data-protection module (
gdpr.erase_contact) has the same unnarrowed counts and is shownto the deleter.
Since #854 the facts are computed by
deletions.facts_forinside the detail views, whichhave
access = READ, and the dialog markup is written into the page for every reader. Aread-only grantee (and a superuser) therefore receives the counts in the page source; the
Delete button in the header is not gated either, so the dialog opens, and only the POST is
refused (404 from
MANAGE).Impact
Another member of the instance (a read-only grantee is enough) learns how many applications,
appointments, contacts, references and sent documents other people hold against a shared
company or listing: counts only, but exactly the kind of data the threat model keeps
per-person. The owner is also misled: "The 3 documents you sent are kept" may count documents
they never sent.
Fix
Application.objects.filter(..., owner=request.user),company.appointments.filter(owner=...),Reference.objects.filter(owner=...),self.object.events.filter(owner=...),self.object.captures.filter(owner=...),RenderedDocumentthrough the requester's applications) and keep only the record-level counts (postings, visible contacts) unnarrowed; say separately, without a number, that what other people keep of their own stays with them as a copy (which is whatKeepsOthersCopiesdoes). Do the same for the counts ingdpr.erase_contact's summary.sharing.panel(...)["manages"], already in the context of both pages, and computefacts_foronly whenmanages, so a read-only grantee is neither offered them nor sent the counts.Done when
tests/security/test_shared_leaks.py::test_the_pages_show_nothing_of_the_owner_salsoasserts that the delete dialog on a shared company and listing counts none of the owner's
applications, appointments or documents, and that a read-only grantee's page has no delete
dialog.
Related
#854 (closed) - moved the facts into the detail pages; says nothing about grantees. #217 (closed) - the delete consequences, written before sharing. SEC2-014 - the contact-erasure summary's counts are also covered there.