Security & Identity 15 Oct 2026 34 min read

Two names, one account: deciding when identifiers are equal

How production systems decide that two usernames, email addresses, package names or file paths are the same identifier, and the four ways that decision fails.

Reconstructs the equality decision behind Spotify's 2013 account hijack, GitHub's and Django's 2019 password-reset collisions, Git's CVE-2014-9390 and CVE-2021-21300, CPython's two urlsplit CVEs and the npm typosquatting record, and the designs that stopped the class: a restricted repertoire, one versioned canonicalizer checked for its fixed point, a unique index on the key, a wider skeleton gate that only rejects, and actions that read the stored record. A reader leaves able to write the equality contract for an identifier, test it against the roughly thousand Unicode aliases of their ASCII namespace, and say where in the system each check has to sit.

The finding that surprised me

The fix the IETF standardised in RFC 8266 (re-apply the rules until the output stops changing, reject after three extra passes) is the workaround Spotify shipped in 2013, because even the successor standard's pipeline is not idempotent; and much of the defensive code in the corpus, such as Git's hard-coded HFS+ ignorables and hashed NTFS short names, is an emulation of somebody else's equality.

What you get out of it

  • Every incident sits on an edge between two stages that decide sameness differently (equality skew), not inside one stage; none of the ten fixes was 'pick a better normalization form'.
  • Order inside the canonicalizer is a security property: on Python 3.11 / Unicode 14.0, lowercase-then-NFKC is non-idempotent for 629 code points and NFKC-then-casefold for none.
  • Acting on the stored record instead of the submitted input is the cheapest fix in the corpus and neutralises the commonest class (GitHub, Django 2019).
  • A producer must enforce its strictest consumer's equality where names enter the shared namespace: Git checks HFS+ and NTFS aliases on push on all platforms, Go escapes uppercase so no store can fold it.
  • Changing the equality relation is a data migration: npm's 2017 lowercase rule skipped 3,815 existing names and left 1,900 open to impersonation until 2023, while PEP 685 measured 73 clashes before changing extras.

Scope

Why this, now. Black Hat USA 2025 showed the validate-before-canonicalize bypass again, six years after HostSplit and twelve after Spotify's postmortem, which makes this an architecture gap rather than a knowledge gap.

What it does not cover. Search collation, display-only text rendering and internationalised email transport (SMTPUTF8), which have different constraints and failure records.

Open the field guide → Self-contained: it loads nothing at read time, follows your system theme, and prints cleanly.