PST-002: A member can pre-claim an unused email address; a password reset or sign-in code then puts its real holder inside the claimant's account #731
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#731
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-002. Severity: Medium.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/config/settings/base.py(243-246)Who can do it, and what they need. Any signed-in member of the instance (an invited colleague, or a stranger where registration is open). Precondition: the target address has no local account yet (a future colleague, somebody the administrator is about to invite). For the sign-in-code leg the administrator must have switched on 'Sign in with a code sent by email'; the reset leg works on every instance that can send mail.
How it works.
Impact. Cross-account: a member chooses a person who does not yet have an account and arranges that this person's documents and personal data are entered into the member's own account, or at minimum that the person can never register their address. Breaks the threat-model promise that 'usernames and addresses are verified before use' for the two credential flows that rely on an address.
Evidence.
Verification (verdict: confirmed; severity after verification: Medium; first-pass claim: High).
The mechanism is real and nothing in Postulo stops it. A signed-in member can store an address nobody holds as an unverified EmailAddress row on their own account through allauth's /accounts/email/ route, and the route stays reachable when the 'Several email addresses' feature is switched off, because addresses.is_offered() only hides the link. allauth then treats that unverified row as a recovery route in two places. ResetPasswordForm.clean_email and RequestLoginCodeForm.clean_email call filter_users_by_email(prefer_verified=True), which falls back to unverified rows when no verified row exists. The address's real holder therefore receives a reset key, or a sign-in code, for the attacker's account. I reproduced all three legs. (1) The victim cannot register: signup redirects to /accounts/confirm-email/ and creates no account. (2) A password reset mailed to the victim lets them set the password on mallory's account, and they then sign in as mallory by the username named in the reset mail. (3) With email sign-in switched on, the code mailed to the victim signs them into mallory's account. I looked for a mitigation and found none. ACCOUNT_FORMS overrides no reset form; AccountAdapter only overrides clean_email for invitations; no Postulo code narrows either lookup to verified rows; no tests/security test pins the safe behaviour (tests/security/test_email_sign_in.py only covers verified addresses). The only attacker is an ordinary member, who is listed in the threat model as an attacker rather than a trusted party. base.py:241 states 'Every address is proven by a link before it is used: to sign in, and to be primary', and reset and code delivery are uses that this does not cover.
I lowered the severity from high to medium for these reasons:
Corrections from verification. File and lines: the setting is at src/postulo/config/settings/base.py:241-246, and the comment at :241 is the claim that does not hold. The real defect is an absence: no override restricts reset or code delivery to verified addresses, and the add-address route is not gated.
Corrections to the auditor's path:
(a) Signup shows no mail in the harness only because the attacker's add consumed the per-address verification-mail rate limit. Once the window passes, the victim does receive an 'Account Already Exists' mail pointing to the password reset, which funnels them into leg 3.
(b) After the reset the victim's address is still unverified on mallory's account. Signing in by that address is stopped at /accounts/confirm-email/, but signing in by the username named in the reset mail succeeds as mallory.
(c) The confirmation link the attacker's add mails to the victim does not log an anonymous browser in. It only marks the address verified on mallory's account.
(d) The code leg needs the administrator-controlled email sign-in switch, which is off by default. The reset and signup legs work on any instance that can send mail.
(e) The attacker gains no pre-existing data of anyone. They gain access to whatever the victim enters into the pre-seeded account, and a standing way back in through their own verified primary address.
Suggested remediation: restrict the reset and login-code lookups to verified addresses, with a custom reset form via ACCOUNT_FORMS and a login-code form or view. Alternatively, do not store a new address on an account until the mailbox owner confirms it, or let a signup or verification of that address by its real holder displace an unverified claim.
Verification evidence.
Suggested remediation. Treat an unverified address as not a credential: override allauth's ResetPasswordForm / RequestLoginCodeForm (or the adapter's user lookup) so that a reset key or a sign-in code is only ever generated for an account on which that address is verified, answering the unknown-address path otherwise. Stop an account from holding somebody else's address indefinitely: expire unverified EmailAddress rows after the confirmation key's own lifetime (a scheduler errand), and/or let a sign-up proceed when the only conflicting rows are unverified (delete them on the newcomer's verification, which allauth already does for duplicates). Consider requiring reauthentication to add an address and enforcing the email-addresses feature switch on the view, not only on the link. Add a tests/security case that pre-claims an address and asserts the holder can still register and that no reset or code is sent to it for another account.
postulo.log#979