Django admin: the per-account login limit counts each spelling of a username, and the email, separately #489

Closed
opened 2026-10-01 10:47:03 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the admin login's rate limit and trying it in a test.

Checked again on 2026-10-01 against main at 0a5cad587: still present.

The admin login's per-account limit (5/300s/key) is keyed on the username exactly as posted. Authentication does not care about capitals and accepts the email address too, so "boss", "Boss", "BOSS" and "boss@example.org" all reach the same account while each has a bucket of its own.

  • Where: core/admin_site.py:45 (key=(request.POST.get("username") or "")[:150]); config/settings/base.py:185-188 (AUTHENTICATION_BACKENDS, which includes allauth's backend) and :207 (ACCOUNT_LOGIN_METHODS = {"username", "email"}); config/settings/base.py:391 (ACCOUNT_RATE_LIMITS).
  • What happens: in a test with the admin mounted, eight wrong passwords for the same spelling from eight addresses gave [200, 200, 200, 200, 200, 429, 429, 429]; nine wrong passwords for nine spellings (boss, Boss, BOSS, …, boss@example.org, BOSS@example.org) from nine addresses were all 200. BOSS, boss@example.org and BOSS@EXAMPLE.ORG with the right password each signed in as boss.
  • Who can trigger it: somebody guessing an administrator's password from many addresses, on an instance whose operator has mounted the admin. Only the per-address limit (10/m/ip) still applies to them.
  • Why it matters: the per-account limit exists to stop exactly this distributed guessing; the comment above the line says "a thousand attempts on one account are counted together however many addresses they come from", and a username of n letters has 2ⁿ spellings, plus the email's. Together with the missing second factor on the same form (reported separately), the admin login is the weakest credential form on the instance.
  • Fix: normalise before consuming: at least .strip().casefold(). Better, resolve the posted login to the account it would authenticate (username case-insensitively, or a verified email) and key on that account's primary key, falling back to the casefolded string when nothing matches, so the username and the email share one bucket. If the admin login is replaced by a redirect to allauth's sign-in, as proposed for the second-factor bypass, this limit goes with it.

Related to #116 (closed), which added this limit.

Done when

  • tests/security/test_admin_exposure.py has a test in which failed attempts spread over case variants of one username and its email address, each from a different address, reach 429 by the sixth; it fails today.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the admin login's rate limit and trying it in a test. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The admin login's per-account limit (`5/300s/key`) is keyed on the username exactly as posted. Authentication does not care about capitals and accepts the email address too, so "boss", "Boss", "BOSS" and "boss@example.org" all reach the same account while each has a bucket of its own. - **Where:** `core/admin_site.py:45` (`key=(request.POST.get("username") or "")[:150]`); `config/settings/base.py:185-188` (`AUTHENTICATION_BACKENDS`, which includes allauth's backend) and `:207` (`ACCOUNT_LOGIN_METHODS = {"username", "email"}`); `config/settings/base.py:391` (`ACCOUNT_RATE_LIMITS`). - **What happens:** in a test with the admin mounted, eight wrong passwords for the same spelling from eight addresses gave `[200, 200, 200, 200, 200, 429, 429, 429]`; nine wrong passwords for nine spellings (`boss`, `Boss`, `BOSS`, …, `boss@example.org`, `BOSS@example.org`) from nine addresses were all `200`. `BOSS`, `boss@example.org` and `BOSS@EXAMPLE.ORG` with the right password each signed in as `boss`. - **Who can trigger it:** somebody guessing an administrator's password from many addresses, on an instance whose operator has mounted the admin. Only the per-address limit (`10/m/ip`) still applies to them. - **Why it matters:** the per-account limit exists to stop exactly this distributed guessing; the comment above the line says "a thousand attempts on one account are counted together however many addresses they come from", and a username of n letters has 2ⁿ spellings, plus the email's. Together with the missing second factor on the same form (reported separately), the admin login is the weakest credential form on the instance. - **Fix:** normalise before consuming: at least `.strip().casefold()`. Better, resolve the posted login to the account it would authenticate (username case-insensitively, or a verified email) and key on that account's primary key, falling back to the casefolded string when nothing matches, so the username and the email share one bucket. If the admin login is replaced by a redirect to allauth's sign-in, as proposed for the second-factor bypass, this limit goes with it. Related to #116 (closed), which added this limit. ## Done when - `tests/security/test_admin_exposure.py` has a test in which failed attempts spread over case variants of one username and its email address, each from a different address, reach `429` by the sixth; it fails today.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:47:03 +00:00
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#489
No description provided.