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.org

The 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.