PST-015: The per-account sign-in lockout reveals which username belongs to which address, and lets a stranger lock anyone out for five minutes #744

Closed
opened 2026-10-10 10:55:53 +00:00 by tiagoagueda · 1 comment
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 8 to 10 October 2026, as PST-015. 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.


  • Severity: Low
  • Category: enumeration
  • Location: src/postulo/accounts/adapter.py (40-61; 71-95)
  • Found by: information disclosure

Who can do it, and what they need. A stranger on the internet who knows one identifier of a target (a username — confirmable through the signup form when registration is open, or seen anywhere a username is printed — or an email address) and wants to confirm the other, or confirm that a candidate address belongs to a known account. Precondition: the sign-in page reachable; six POSTs within allauth's 10/min/IP limit, or from more than one address.

How it works.

  1. POST five wrong passwords with login=alice (one known username). allauth's login_failed limit (5/5m/key) is now spent under the key account:<alice.pk> because _get_login_attempts_cache_key resolves whatever was typed to the account id.
  2. POST one wrong password with login=candidate@example.org. If that address belongs to alice (primary email or any allauth EmailAddress, verified or not), the key resolves to the same account:<pk> and the answer is "Too many failed login attempts. Try again later."; any other address — another account's or none — answers "The username and/or password you specified are not correct." Verified by scratch test: alice@example.org → locked message; bob@example.org and nobody@example.org → normal failure. The reverse direction works the same (lock under the email, then probe candidate usernames). allauth's own per-key limit (keyed on the typed string) did not have this property; the project's change (#489) introduced the cross-identifier linkage.

Impact. An attacker who has a username (public-ish) can confirm which email address it belongs to, or confirm that an email address has an account on this instance when combined with any other identifier; it also provides a five-minute lockout of any targeted account from the outside (DoS, which the threat model excludes). This weakens the stated promise that the sign-in surface "never confirms whether a record exists".

Evidence.

src/postulo/accounts/adapter.py:91-95: `named = (credentials.get("email") or credentials.get("username") or "").strip(); account = account_named(named); if account is None: return f"name:{named.casefold()}"; return f"account:{account}"`. :54-60 `EmailAddress.objects.filter(email__iexact=login)... or people.filter(email__iexact=login)... or people.filter(username__iexact=login)`. Scratch test output: `HERS: True  NOT HERS: False  UNKNOWN: False`.

Verification (verdict: confirmed; severity after verification: Low). Keep low. Precision: it links two identifiers of one account, which needs one already known; it does not on its own reveal that an email or username has an account. The DoS aspect is excluded by the threat model. Possible remediation: keep per-account counting but make the visible response to a locked key identical to the normal failure (the lock still stops the guess), or add a per-typed-string count beside the per-account one.

Suggested remediation. Make the refusal indistinguishable: once any key for the typed identifier is spent, answer the generic "not correct" sentence (or always show the lockout sentence only when the per-name key is spent as well), or count under both the account key and the name key and refuse on either while rendering the same text. Alternatively keep the account-keyed count server-side but never reveal it: on lockout, silently reject with the ordinary message.

Also covered by this issue.

  • Any account can be locked out of password sign-in for five minutes by a stranger, because failed attempts are counted per account rather than per typed name (src/postulo/accounts/adapter.py (71-95); Informational). Denial of password sign-in for a chosen person; the threat model excludes denial of service, so this is recorded rather than rated. Noted because the same mechanism produces the linkage oracle above.
Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 8 to 10 October 2026, as **PST-015**. 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. --- - **Severity:** Low - **Category:** enumeration - **Location:** `src/postulo/accounts/adapter.py` (40-61; 71-95) - **Found by:** information disclosure **Who can do it, and what they need.** A stranger on the internet who knows one identifier of a target (a username — confirmable through the signup form when registration is open, or seen anywhere a username is printed — or an email address) and wants to confirm the other, or confirm that a candidate address belongs to a known account. Precondition: the sign-in page reachable; six POSTs within allauth's 10/min/IP limit, or from more than one address. **How it works.** 1. POST five wrong passwords with `login=alice` (one known username). allauth's `login_failed` limit (`5/5m/key`) is now spent under the key `account:<alice.pk>` because `_get_login_attempts_cache_key` resolves whatever was typed to the account id. 2. POST one wrong password with `login=candidate@example.org`. If that address belongs to alice (primary email or any allauth EmailAddress, verified or not), the key resolves to the same `account:<pk>` and the answer is "Too many failed login attempts. Try again later."; any other address — another account's or none — answers "The username and/or password you specified are not correct." Verified by scratch test: `alice@example.org` → locked message; `bob@example.org` and `nobody@example.org` → normal failure. The reverse direction works the same (lock under the email, then probe candidate usernames). allauth's own per-key limit (keyed on the typed string) did not have this property; the project's change (#489) introduced the cross-identifier linkage. **Impact.** An attacker who has a username (public-ish) can confirm which email address it belongs to, or confirm that an email address has an account on this instance when combined with any other identifier; it also provides a five-minute lockout of any targeted account from the outside (DoS, which the threat model excludes). This weakens the stated promise that the sign-in surface "never confirms whether a record exists". **Evidence.** ```text src/postulo/accounts/adapter.py:91-95: `named = (credentials.get("email") or credentials.get("username") or "").strip(); account = account_named(named); if account is None: return f"name:{named.casefold()}"; return f"account:{account}"`. :54-60 `EmailAddress.objects.filter(email__iexact=login)... or people.filter(email__iexact=login)... or people.filter(username__iexact=login)`. Scratch test output: `HERS: True NOT HERS: False UNKNOWN: False`. ``` **Verification** (verdict: confirmed; severity after verification: Low). Keep low. Precision: it links two identifiers of one account, which needs one already known; it does not on its own reveal that an email or username has an account. The DoS aspect is excluded by the threat model. Possible remediation: keep per-account counting but make the visible response to a locked key identical to the normal failure (the lock still stops the guess), or add a per-typed-string count beside the per-account one. **Suggested remediation.** Make the refusal indistinguishable: once any key for the typed identifier is spent, answer the generic "not correct" sentence (or always show the lockout sentence only when the per-name key is spent as well), or count under both the account key and the name key and refuse on either while rendering the same text. Alternatively keep the account-keyed count server-side but never reveal it: on lockout, silently reject with the ordinary message. **Also covered by this issue.** - **Any account can be locked out of password sign-in for five minutes by a stranger, because failed attempts are counted per account rather than per typed name** (`src/postulo/accounts/adapter.py` (71-95); Informational). Denial of password sign-in for a chosen person; the threat model excludes denial of service, so this is recorded rather than rated. Noted because the same mechanism produces the linkage oracle above.
tiagoagueda added this to the 0.6.0 milestone 2026-10-10 10:57:26 +00:00
Author
Owner

Fixed in 63532a151 on main.

Fixed in 63532a151 on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#744
No description provided.