PST-003: An invitation bound to an address marks that address verified at sign-up although Postulo never delivered the invitation #732
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#732
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-003. 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/accounts/signals.py(67-90)Who can do it, and what they need. Whoever the administrator hands the link to, or anyone who finds it on the channel it was handed over on. Precondition: an invitation with an email filled in.
How it works.
Impact. Mis-delivery of an invitation produces an account with a 'proven' address that was never proven, contradicting the threat model's 'addresses verified before use' and blocking the real holder; limited to administrator error or a leaked link.
Evidence.
Verification (verdict: confirmed; severity after verification: Medium; first-pass claim: Low).
The code does what the finding says. consume_invite (signals.py:83-87) marks the typed address verified when it equals the invitation's address, and Postulo never delivers the invitation. InviteCreateView only renders the link to the administrator, and invite_created.html says 'Send the link however you like'. The docstring's premise that the invitation 'was delivered to that address' is therefore not enforced. With ACCOUNT_EMAIL_VERIFICATION='mandatory', verification would otherwise be the only thing stopping a link holder who is not the invitee. The project's own intent (adapter.clean_email docstring and the tests/test_invites.py header: an invitation addressed to one person should not be redeemable by whoever else ends up holding the link) is defeated, because the holder simply types the bound address and is signed in at once. The finding understates the impact. allauth's defence against pre-account-hijacking is wipe_password(), which clears the password of a local account whose address is NOT verified when an SSO login with the same provider-verified address lands on it. A falsely verified address skips that defence. With OIDC configured and POSTULO_OIDC_LINK_BY_EMAIL on (the default), the real invitee who later signs in via SSO is connected to the holder's account, and the holder keeps a working password. Without OIDC the effect is address squatting, and the real person can still use password reset to take the account over, so the finding's 'can never register' is overstated. The unproven address and the neutralised wipe_password were demonstrated by scratch test. The SSO login step was exercised at the allauth-function level (authenticate_by_email plus wipe_password), not through a full OIDC round-trip.
Corrections from verification. Attacker: anyone who obtains an email-bound invitation link before the invitee uses it (wrong recipient, group chat, chat log). Preconditions: a bound invitation handed over by a channel other than mail to that mailbox. For the account-takeover amplification, additionally OIDC configured with link-by-email on (the default once OIDC is set up), and the invitee later signing in via SSO. Effect: the holder gets a signed-in account carrying a falsely verified address and bypasses the binding. Where SSO linking applies, the invitee is later signed into the holder's account (allauth's pre-hijack wipe skipped), so the holder shares access to whatever the invitee stores there. Without OIDC it is address squatting only. Correct step 4: password reset mails go to the real person, who can take the account over that way. Remediation: do not mark the address verified from the invite alone. Either mail the invitation to the bound address (making the docstring true) or let allauth send the normal verification mail for bound invitations, and keep the verified shortcut only if Postulo delivers the link itself. Lines signals.py:67-90 and views.py:395-420 are correct, and the binding is enforced in adapter.py:119-135.
Verification evidence.
Suggested remediation. Either send the invitation to the bound address from Postulo (then the link is proof) or leave the address unverified and let allauth's mandatory verification run as for any other sign-up; say on the invite page which of the two applies.