PST-097: The first-account-becomes-administrator check may be racy on PostgreSQL #826
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#826
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-097. Severity: Informational (not settled: see below).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/signals.py(50-64)Who can do it, and what they need. Two people (or one person with two browsers) who both reach the sign-up page of a freshly installed, still-empty instance on PostgreSQL and submit within the same few milliseconds. Precondition: the instance has zero accounts (signup_open_now() is True via site.is_empty()), and PostgreSQL's default READ COMMITTED isolation.
How it works.
User.objects.exclude(pk=user.pk).exists(); B's row is uncommitted and invisible, so A is made is_staff and is_superuser.Impact. An uninvited person who happens to race the installer on first start becomes a superuser. The window is tiny and only exists before the first account commits, so the path is speculative; recorded because a superuser is the highest privilege in the system.
Evidence.
Verification (verdict: uncertain; severity after verification: Informational; first-pass claim: Low). Missing to settle it: two real PostgreSQL connections, each running a sign-up transaction on an empty database from different client IPs, checking whether both end up is_superuser. If it is real, the fix is to take a lock around the check (select_for_update on the SiteSettings row, or a PostgreSQL advisory lock) or to make the first-administrator decision from a unique constraint. The scenario should be restated as 'two overlapping sign-ups from different addresses', since the same-IP case is probably serialised by the database cache.
Suggested remediation. Take a lock before deciding (e.g. select_for_update on the SiteSettings row, which is the natural instance-wide mutex and is already read on signup), or use a database-level guard such as a partial unique index on is_superuser for the bootstrap case, so that only one transaction can win the 'first account' decision.
Already fixed by the lock added for #738 (9f6c2e0 and its tests, in the 0.6.0 round): the first-account check takes select_for_update on the instance's one settings row inside a transaction, so a second sign-up waits for the first to commit and then finds an account. SQLite serialises writers anyway. Nothing further to change.