The Reset Mail Sent to a Look-Alike — Python Bug Hunt
Modelled on Django CVE-2019-19844 (December 2019). Django's password-reset form found accounts with a case-insensitive database lookup, and Unicode case…
- Language: Python
- Layer: Backend
- Difficulty: Medium
- Concepts: Security, Unicode, Auth
- Modelled on: Django · CVE-2019-19844
- Visible tests: the account's own address gets the link; a dotless-i look-alike gets nothing
- Reward: 50 XP for a complete fix
Briefing
Modelled on Django CVE-2019-19844 (December 2019). Django's password-reset form found accounts with a case-insensitive database lookup, and Unicode case mapping lets different strings compare equal — a dotless ı uppercases to a plain I. The form then mailed the reset link to the address as typed, not the address on the account, so an attacker could request a reset for a look-alike of a victim's email and receive the victim's reset token. The fix sends mail only to the stored address and re-checks the match with Unicode normalisation and case folding.
This reconstruction's send_reset has both halves of the original bug.
Fix send_reset.
Bug report
BUG-CVE-2019-19844 · Priority: Critical (account takeover) · Reported by: security
send_reset(email, users, outbox) appends one message per matching ACTIVE account, of the form {"to": <the account's stored email>, "username": ...}.
- candidates come from db.users_with_email_iexact (the database lookup)
- a candidate only matches if NFKC-normalised + casefolded, the typed address equals the stored one (so "MIKE@Example.ORG" matches "mike@example.org", but "mıke@example.org" with a dotless i does not)
- mail never goes to the typed address; unknown or inactive accounts get nothing
Observed: a reset for a look-alike address was delivered to the attacker's mailbox carrying a valid token for the victim's account.
Logs
[reset] lookup email__iexact="mıke@example.org" -> user mike
[reset] sent reset link for user=mike to=mıke@example.orgThe code as shipped
src/accounts/reset.py (editable)
db = bug_require("./db.py")
def send_reset(email, users, outbox):
for user in db.users_with_email_iexact(users, email):
if not user["active"]:
continue
outbox.append({"to": email, "username": user["username"]})
Read-only context: src/accounts/db.py.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Python bug hunts.