PST-009: On a fresh instance, sign-up is open and the first account becomes superuser, so whoever arrives first owns the instance #738
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#738
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-009. 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/signals.py(50-62);src/postulo/core/site.py(289-296)Who can do it, and what they need. A stranger who reaches a freshly deployed (or fully emptied) instance before the operator; for the race, two strangers at once.
How it works.
signup_open_now()returns True whileUser.objects.exists()is False, whatever POSTULO_REGISTRATION_OPEN says.user_signed_upsets is_staff and is_superuser and marks the address verified without any email round-trip.User.objects.exclude(pk=user.pk).exists()runs inside each request's transaction; under PostgreSQL READ COMMITTED two overlapping sign-ups each see no other row and both become superuser (no lock/unique guard, unlike the invite path which takes select_for_update).manage.pyor a restore of an empty DB can) reopens the door.Impact. Instance takeover by a race to the sign-up form on a new deployment exposed before the operator signs up; documented as intended for the installer, but the window is not bounded and the race is not closed.
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Keep as a narrow hardening gap. (a) The first-account bootstrap itself is documented intended behaviour, not a vulnerability; its only exposure is the time between the instance being reachable and the operator signing up, and the README order plus the loopback port binding keep that closed for a documented install. (b) The unlocked check in accounts/signals.py:58 matters only on PostgreSQL (docker/compose.postgres.yml); SQLite serialises requests. (c) Step 4 is wrong as stated for the application: PersonDeleteView, DeleteAccountView and
manage.py delete_accountall refuse the last administrator; only raw database or restore access empties the instance, which is operator-level and out of scope. Remediation: take a lock for the check (for example select_for_update on the SiteSettings row, or a PostgreSQL advisory lock) or re-check after commit, and optionally let an operator disable the empty-instance bootstrap.Suggested remediation. Have the first sign-up require a bootstrap secret printed by the entrypoint/
createsuperuser(or POSTULO_BOOTSTRAP_TOKEN), or at least take a table-level lock /select_for_updateon a sentinel row before deciding who is first; log the event at WARNING.Also covered by this issue.
src/postulo/core/site.py(289-296); Low). Complete takeover of the instance at bootstrap. The window is usually short, but on a public deployment it is a race the operator may not know they are in; the comment assumes 'the person who just installed Postulo is the only one who can reach it'.src/postulo/core/site.py(289-296); Informational). Documented and deliberate ('somebody has to'), and bounded to the first minutes of an install; recorded so the synthesis can say it was considered. Two overlapping first sign-ups could both become administrators, since the check is not serialised.Fixed in
b4fa62b67on main.