PST-020: The Django admin's profile inline exposes and edits every account's birth data, nationalities and gender #749
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#749
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-020. 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/accounts/admin.py(10-16; 10-16,41-48)Who can do it, and what they need. A superuser (the instance's first account, or one made with createsuperuser) on an instance where the operator has mounted Django's admin (POSTULO_ADMIN_URL set; off by default). An administrator appointed through People is only is_staff and sees an empty admin index, so this does not reach them.
How it works.
Impact. docs/THREAT-MODEL.md says the mounted admin 'holds accounts, invitations and token metadata and no person's records (#368)' and that date/place of birth, nationalities and gender are 'never in a list' and the most identifying things a profile holds. tests/security/test_admin_exposure.py walks only OwnedModel subclasses, so Profile (a OneToOne on User, not an OwnedModel) escapes the check. A superuser can also silently rewrite these columns (and a person's table/dashboard preferences) with no event or notice to the person. …
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Attacker: only a superuser on an instance whose operator mounted the admin. Superuser status is not handed out by People, so this is the operator or someone the operator promoted in the admin itself. It is a gap against the documented boundary (#368) rather than an escalation, because that party can already read the database. Fix: drop the inline or restrict it to non-sensitive preference fields, and extend the exposure test to walk every model with a relation to the user (Profile) or assert the inline's field list.
Suggested remediation. Either drop ProfileInline from UserAdmin (the admin's stated job is account metadata), or restrict it with an explicit
fieldstuple limited to non-personal preference columns (language, time_zone, theme) and mark the rest readonly or excluded. Extend test_admin_exposure.py's walk to assert that no admin form, inline included, offers the columns core/personal.py governs.Also covered by this issue.
src/postulo/accounts/admin.py(10-16,41-48); Informational). The threat model names date/place of birth, nationality and gender as 'the most identifying things a profile can hold' and promises they are 'never in a list ... an administrator's'. The admin shows and lets a superuser edit them for everyone, and edits bypass core/personal.py's rules. …src/postulo/accounts/admin.py(10-16,41-48); Informational). The most identifying columns the application holds (identity-check data, and nationality/gender which the threat model treats as sensitive) are listed for every member on one page of the mounted admin, contrary to the documented boundary for administrators. …Fixed in
2dcd203aaon main.