Signing in by email code: the guess-limit, enumeration and second-factor tests cannot fail #631
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#631
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 on 2026-09-29 in a read-only review of postulo at
95348908d, running the assertions oftests/security/test_email_sign_in.pyagainst altered settings.Related to #153.
Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now attests/security/test_email_sign_in.py:102-113(test_a_code_does_not_skip_an_authenticator),:143-151(test_asking_about_an_address_nobody_has_says_the_same_thing),:154-161(test_guessing_the_code_runs_out) and:199-201(_asked_for_a_code): two lines up, the three tests themselves unchanged.Three tests in
tests/security/test_email_sign_in.pypass whether or not the property they name holds.1. The guess limit
test_guessing_the_code_runs_out(:156-163) posts000000three times and asserts that the person is not signed in. Any wrong code leaves the person signed out, limit or none, and the test never sends the right code afterwards. WithACCOUNT_LOGIN_BY_CODE_MAX_ATTEMPTS=1000its assertion still holds, and the real code then signs the person in. With the limit at 2, the real code is refused after three guesses. The test cannot tell these two cases apart.2. Enumeration
test_asking_about_an_address_nobody_has_says_the_same_thing(:145-153) compares status codes and_asked_for_a_code(:201-203), which is"code" in content. The request page contains "code" too. With allauth'sACCOUNT_PREVENT_ENUMERATION=False, a fresh client asking aboutnobody@example.orggets the form back with "The email address is not assigned to any user account." and status 200, and both of the test's comparisons are still equal. The test also uses one client for both requests. After the known address, the session holds a pending code login and the second request is redirected to the confirm page before the address is looked at, so the unknown address is never really asked.3. The second factor
test_a_code_does_not_skip_an_authenticator(:104-115) ends with"authenticator" in ... or response.redirect_chain, which any redirect satisfies. Today the chain ends at/accounts/2fa/authenticate/, but the assertion would accept any redirect. The first assertion (_auth_user_idnot in the session) still guards the main property.tests/security/test_email_sign_in.py:104-115,:145-153,:156-163,:201-203.tests/security/. The tests that represent them prove neither.MAX_ATTEMPTSwrong guesses, post the real code and assert that it is refused, with a control showing that the real code works without the guesses. (2) Use two separate clients, or flush the session between the requests. Assert that both end atreverse("account_confirm_login_code"), that neither response carries form errors, and that only the known address produced a message inmail.outbox. (3) Assert that the last entry ofredirect_chainisreverse("mfa_authenticate").Done when
override_settings(ACCOUNT_LOGIN_BY_CODE_MAX_ATTEMPTS=1000)the rewritten guess-limit test fails, and underACCOUNT_PREVENT_ENUMERATION=Falsethe rewritten enumeration test fails. Both pass with the shipped settings.