PST-030: The company identifier formset discloses whether another account's identifier row id exists #759
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#759
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-030. 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/jobs/forms.py(491-540)Who can do it, and what they need. Any signed-in account on the same instance. Precondition: none beyond a session and one company of their own (or the create form). Verified with a scratch test under /tmp/.../verify/own-jobs/test_identifier_row_oracle.py and test_more.py.
How it works.
idModelChoiceField it adds validates against CompanyIdentifier._default_manager — every account's rows.Impact. Confirms the existence (and by enumeration the count and growth) of other accounts' company-identifier rows, contradicting the threat model's rule that another person's record is never confirmed to exist. No content is read and nothing is modified: the foreign row is never written to or deleted (verified, including with DELETE ticked).
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Lines are right: forms.py:491-540 and views.py:388-418 (get_identifiers at 388, refuse_bad_identifiers at 405). Precondition: any signed-in account; the create view works too, but it creates a company as a side effect. The only information disclosed is whether an identifier row with integer pk N exists on the instance, not whose it is or what it holds. Batching with TOTAL_FORMS=k only answers 'at least one is missing', so bisection is needed. Remediation: add RowsAlreadyGone to BaseCompanyIdentifierFormSet and add the company identifier formset to tests/security/test_formset_rows.py.
Suggested remediation. Add
RowsAlreadyGonetoBaseCompanyIdentifierFormSetexactly asBasePersonIdentifierFormSetdoes, so a made-up key and another person's key both become a silently passed-over row and the page answers the same way for either. Extend tests/security/test_formset_rows.py to the company form (jobs:company_update and jobs:company_create) and, for every inline formset, assert that a non-existent id and another account's id produce the same status code and the same state.Fixed in
2dd11a081on main.