SEC2-031: The company merge preview lists every member's appointments at the other company, not only the viewer's #973
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#973
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: Low · Category: Data Exposure · Reference: CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor); OWASP A01:2021 (Broken Access Control)
Location:
src/postulo/jobs/merging.py:428(_moved(_("Appointments"), other.appointments.all(), str)inplan_companies)src/postulo/jobs/merging.py:399, 416-427(mine = _seen_by(viewer); the career rows beside it areother.<relation>.filter(**mine))src/postulo/applications/models.py:992-994(Appointment.__str__: the company's name and the meeting's date and time)src/postulo/jobs/views.py:1285-1313, 1370(MergeViewovermanageable_by(self.request.user), so any member of the owning group may merge two of the group's companies)Issue
plan_companiesbuilds the list of what a merge would move. The person-owned rows next to it (career, education, certifications, honours, memberships, courses) are narrowed withfilter(**mine), whereminecomes from_seen_by(viewer); the appointments row isother.appointments.all(), named bystr(), which is the company's name and the meeting's date and time. On a group-owned employment-service company, the member who opens the merge preview is shown when every other member meets their adviser. Moving all of them (merging.py:584,other.appointments.update(company=kept)) is right; naming them to the viewer is not. (The delete confirmation atjobs/views.py:1413countscompany.appointments.count()across owners too, but a count of what a cascade will remove is the honest consequence and carries no detail.)Impact
A group member learns the dates and times of the other members' appointments at a shared employment service: existence and timing only, not the notes or the place.
Fix
_moved(_("Appointments"), other.appointments.filter(**mine), str)(the viewer's own), or keep the whole set but name it by count only. Done when: a test intests/test_merging.py(ortests/security/test_shared_leaks.py) gives two members of a group an appointment each at a group-owned company and asserts the merge plan names only the viewer's.