Evidence ledger
One row per claim in Two names, one account: deciding when identifiers are equal: who published it, what grade it carries, when it was written, when the link was last checked, and the quote or figure it rests on. Nothing in the guide is cited from memory, so anything not in this table is not in the guide.
One row per claim. Tier grades follow the skill's hierarchy (postmortem > source/adr >
paper/talk > casestudy/blog > vendor).
Access note for auditors. This guide was researched on 2026-10-15 from a sandbox whose egress policy allowed direct fetches only to github.com / raw.githubusercontent.com. Every GitHub-hosted source (Git, CPython, Django, Go x/mod, Go x/text, Twisted, the Python security database, the PEP and Rust RFC texts, Chromium's IDN policy, the Unicode confusables data files) was fetched directly in this session and the quotes below are copied from the fetched files. All other sources were retrieved through search-engine retrieval of the live pages in this session; quotes for those rows are copied from that retrieved text, not from memory, and the row says when a claim rests on secondary reporting. Rows marked derived are computations run in this session and are reproducible with the script in section 7 of the guide.
| # | Org | Title | Tier | Published | Checked | URL | Claim I take from it | Supporting quote or figure |
|---|---|---|---|---|---|---|---|---|
| 1 | Spotify | Creative usernames and Spotify account hijacking | postmortem | 2013-06-18 | 2026-10-15 | https://engineering.atspotify.com/2013/06/creative-usernames | A reset requested for ᴮᴵᴳᴮᴵᴿᴰ was mailed to the account whose canonical name was BIGBIRD, and redeeming the link canonicalized again to bigbird, so the new password landed on a third account. | "When resetting the password for 'ᴮᴵᴳᴮᴵᴿᴰ' canonical_username was applied once ... However, when the link was used, canonical_username was once again applied, yielding 'bigbird'." (as retrieved) |
| 2 | Spotify | Same post, root cause | postmortem | 2013-06-18 | 2026-10-15 | https://engineering.atspotify.com/2013/06/creative-usernames | The design relied on idempotence of Twisted's nodeprep.prepare, which did not hold for input outside Unicode 3.2. | "We were relying on nodeprep.prepare being idempotent, and it wasn't." ; "nodeprep.prepare did not check the input, and neither did we." (as retrieved) |
| 3 | Spotify | Same post, the fix | postmortem | 2013-06-18 | 2026-10-15 | https://engineering.atspotify.com/2013/06/creative-usernames | The interim fix was a fixed-point check at registration plus a search for accounts whose stored canonical name was wrong; account creation was disabled meanwhile. | only allowed registering username X if X equaled canonical_username(X); "disabled account creation until we could prevent the attack" (as retrieved) |
| 4 | Twisted | xmpp_stringprep.py (current trunk) | source | 2013-onward | 2026-10-15 | https://github.com/twisted/twisted/blob/trunk/src/twisted/words/protocols/jabber/xmpp_stringprep.py | The profile maps first, NFKC-normalizes second, then checks; the module pins Unicode 3.2 and deprecated the crippled flag in 13.1.0. |
result = self.map(string) then unicodedata.normalize("NFKC", result); "# We require Unicode version 3.2."; Version("Twisted", 13, 1, 0), "crippled is always False" |
| 5 | (derived) | Non-idempotence count, Python 3.11.17 / Unicode 14.0.0 | derived | 2026-10-15 | 2026-10-15 | (script in guide, section 7) | Over every assigned code point, NFKC(lower(x)) is not idempotent for 629 code points; NFKC(casefold(x)) for 637; casefold(NFKC(x)) for 0 single code points. 998 non-ASCII code points fold to a single ASCII letter or digit. | script output: lower_then_nfkc_nonidem: 629, casefold_then_nfkc_nonidem: 637, django_nonidem: 0; 998; f1('ᴮᴵᴳᴮᴵᴿᴰ') = 'BIGBIRD', f1(f1(...)) = 'bigbird' |
| 6 | Go project | golang.org/x/text secure/precis profile.go | source | 2017-onward | 2026-10-15 | https://github.com/golang/text/blob/master/secure/precis/profile.go | The PRECIS successor standard now tells implementers to iterate to a fixed point and reject if it does not stabilise after three extra passes; Go repeats four times. | "RFC 8266 §2.1: Implementation experience has shown that applying the rules for the Nickname profile is not an idempotent procedure ... SHOULD apply the rules repeatedly until the output string is stable; if the output string does not stabilize after reapplying the rules three (3) additional times ... reject the input string as invalid." ; r = 4 |
| 7 | IETF | RFC 8266, Preparation, Enforcement, and Comparison of Internationalized Strings Representing Nicknames | adr | 2017-10 | 2026-10-15 | https://www.rfc-editor.org/rfc/rfc8266 | Same rule, at its source (quoted verbatim in row 6 by the Go implementation). | see row 6 |
| 8 | GitHub / John Gracey (Wisdom) | Hacking GitHub's Auth with Unicode's Turkish Dotless 'I' | postmortem | 2019-11-28 | 2026-10-15 | https://dev.to/jagracey/hacking-github-s-auth-with-unicode-s-turkish-dotless-i-460n | GitHub's forgot-password flow matched an attacker-typed address containing ı to the victim's address after case conversion, then mailed the token to the typed address. | "trigger a password reset email to be delivered to the wrong email address" ; "The reset link went to the email address provided by the attacker" (as retrieved via Bugcrowd/Hackaday summaries; the dev.to page itself could not be fetched from this sandbox) |
| 9 | Hackaday | This Week In Security: Unicode, Truecrypt, And NPM Vulnerabilities | blog | 2019-12-20 | 2026-10-15 | https://hackaday.com/2019/12/20/this-week-in-security-unicode-truecrypt-and-npm-vulnerabilities/ | Corroboration of the GitHub case-collision write-up; secondary accounts disagree on whether GitHub upper- or lower-cased. | Bugcrowd describes uppercasing; Medium and Hackaday describe lowercasing (as retrieved) |
| 10 | Django | Django 3.0.1 release notes, CVE-2019-19844 | postmortem | 2019-12-18 | 2026-10-15 | https://github.com/django/django/blob/main/docs/releases/3.0.1.txt | Same class of bug in Django's password reset; fix both tightened the comparison and switched the delivery address to the one on record. | "Unicode characters, that compared equal to an existing user email when lower-cased for comparison, an attacker could be sent a password reset token" ; "the email containing the reset token will be sent to the email address on record rather than the submitted address." |
| 11 | Django | django/contrib/auth/forms.py | source | 2019-onward | 2026-10-15 | https://github.com/django/django/blob/main/django/contrib/auth/forms.py | The comparison is NFKC first, then casefold, per UTR #36 §2.11.2(B)(2); the mail goes to getattr(user, email_field_name). |
unicodedata.normalize("NFKC", s1).casefold() == unicodedata.normalize("NFKC", s2).casefold() ; user_email = getattr(user, email_field_name) |
| 12 | Git | Git v2.2.1 release notes (CVE-2014-9390) | source | 2014-12-17 | 2026-10-15 | https://github.com/git/git/blob/master/Documentation/RelNotes/2.2.1.adoc | A path accepted on a case-sensitive filesystem clobbered .git/config when checked out on a case-insensitive one; HFS+ ignorable code points and NTFS short names created more aliases. The fix rejects on affected systems and checks on push on all. | "would have clobbered \".git/config\"" ; "\".g\u200cit/config\" is treated as if it were \".git/config\"" ; "this check is enabled on all platforms" ; credit to "Matt Mackall and Augie Fackler" |
| 13 | Git | utf8.c, next_hfs_char | source | 2014-onward | 2026-10-15 | https://github.com/git/git/blob/master/utf8.c | Git hard-codes the 16 code points HFS+ ignores, and its own comment says the emulation is incomplete. | "Pick the next char from the stream, ignoring codepoints an HFS+ would. Note that this is not complete by any means." ; cases 0x200c ... 0xfeff |
| 14 | Git | path.c, is_ntfs_dot_generic | source | 2014-onward | 2026-10-15 | https://github.com/git/git/blob/master/path.c | Git emulates NTFS trailing-dot/space stripping and 8.3 short names, including hashed short-name prefixes. | "Is it a regular NTFS short name, i.e. shortened to 6 characters, followed by ~1, ... ~4?" ; is_ntfs_dot_str(name, "gitmodules", "gi7eba") |
| 15 | Git | Git v2.17.6 release notes (CVE-2021-21300) | source | 2021-03-09 | 2026-10-15 | https://github.com/git/git/blob/master/Documentation/RelNotes/2.17.6.adoc | Seven years later the same filesystem-equality seam produced remote code execution during clone, backported to 14 release lines. | "On case-insensitive file systems with support for symbolic links ... Git could be fooled into running remote code during a clone." ; 2.30.2 notes list fixes in v2.17.6 through v2.29.3 |
| 16 | Python Security Response Team | python-security vulnerabilities.yaml, CVE-2019-9636 and CVE-2019-10160 | source | 2019-02-16 / 2019-06-03 | 2026-10-15 | https://github.com/vstinner/python-security/blob/master/vulnerabilities.yaml | urlsplit's netloc could gain new separators under NFKC; the first fix itself had a gap that needed a second CVE about 3.5 months later. | "URLs encoded with Punycode/IDNA use NFKC normalization to decompose characters. This can result in some characters introducing new segments into a URL." ; "the fix ignored the user/password before @ whereas it still allowed to exploit the vulnerability." |
| 17 | CPython | Lib/urllib/parse.py, _checknetloc | source | 2019-onward | 2026-10-15 | https://github.com/python/cpython/blob/main/Lib/urllib/parse.py | The parser now pre-computes the downstream (IDNA) equivalence and refuses input whose normalized form contains separators. | "looking for characters like \u2100 that expand to 'a/c'" ; "IDNA uses NFKC equivalence, so normalize for this check" |
| 18 | Microsoft / Jonathan Birch | HostSplit: Exploitable Antipatterns in Unicode Normalization (Black Hat USA 2019) | talk | 2019-08 | 2026-10-15 | https://i.blackhat.com/USA-19/Thursday/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization.pdf | URL strings parse as one host and resolve as another; RFC 3490 already said to run ToASCII and fail on mismatch; reported in Edge/IE, .NET, Python, Java, Office 365 and Gmail. | "parsed as belonging to one hostname but resolved as belonging to a different host name" ; RFC 3490 "fail if the result doesn't match our input" (as retrieved from slides and whitepaper; slide numbers not available) |
| 19 | Akamai / Ryan Barnett | Lost in Translation: Exploiting Unicode Normalization (Black Hat USA 2025) | talk | 2025-08 | 2026-10-15 | https://i.blackhat.com/BH-USA-25/Presentations/USA-25-Barnett-Lost-In-Translation-Exploiting-Unicode-compressed.pdf | Six years on, the same class (CWE-180, validate before canonicalize) still bypasses WAFs and filters via confusables, truncation, case mapping and combining marks. | agenda "decoding errors, truncation, confusables, casing, combining diacritics" ; "CWE-180: Incorrect Behavior Order: Validate Before Canonicalize" (as retrieved; slide numbers not available) |
| 20 | Xudong Zheng | Phishing with Unicode Domains | blog | 2017-04-14 | 2026-10-15 | https://xudongz.com/blog/2017/idn-phishing/ | A whole-script Cyrillic label displayed as apple.com in Chrome, Firefox and Opera because browser filters only caught mixed-script labels. | "xn--80ak6aa92e.com" displays as "apple.com"; fixed "as of Chrome 59" (as retrieved; press accounts say Chrome 58) |
| 21 | Chromium | docs/idn.md, IDN display policy | adr | current | 2026-10-15 | https://github.com/chromium/chromium/blob/main/docs/idn.md | Chrome now decides display by script-mixing rules, whole-script confusables and a skeleton comparison against top domains, falling back to punycode. | "If the skeleton of the registrable part of a hostname is identical to one of the top domains ... show punycode." ; "In a perfect world, domain registrars would not allow these confusable domain names to be registered." |
| 22 | Unicode Consortium | confusables.txt, UTS #39 data, versions 16.0.0, 17.0.0, 18.0.0 (dev) | source | 2024 / 2025-07-22 / 2026-08-06 | 2026-10-15 | https://github.com/unicode-org/unicodetools/blob/main/unicodetools/data/security/17.0.0/confusables.txt | The visual-equivalence relation grows each release: 6,355 mappings in 16.0.0, 6,565 in 17.0.0, 6,712 in the 18.0.0 development file. | counted lines beginning with a hex code point: 6355 / 6565 / 6712; header "Version: 17.0.0", "Date: 2025-07-22" |
| 23 | Cambridge (Boucher, Anderson) | Trojan Source: Invisible Vulnerabilities (USENIX Security 2023) | paper | 2021-10-30 preprint; 2023-08 | 2026-10-15 | https://www.usenix.org/conference/usenixsecurity23/presentation/boucher | The compiler's identity of a token and the reviewer's identity of it can be made to differ via bidi controls and homoglyphs; demonstrated in 12 languages, disclosed to 19 organisations. | "C, C++, C#, JavaScript, Java, Rust, Go, Python, SQL, Bash, Assembly, and Solidity" ; "coordinated disclosure with 19 organizations" ; CVE-2021-42574, CVE-2021-42694 (as retrieved) |
| 24 | Waseda / NTT (Suzuki et al.) | ShamFinder: An Automated Framework for Detecting IDN Homographs (IMC 2019) | paper | 2019-09 | 2026-10-15 | https://arxiv.org/abs/1909.07539 | The Unicode confusables list alone is not sufficient for detecting homographs; the authors build a larger homoglyph database (SimChar) from Unicode 12.0.0 and combine the two. | "homoglyph database ... combines the existing Unicode Confusables list with the authors' own SimChar database built from Unicode 12.0.0" (as retrieved, summary of slides) |
| 25 | npm | crossenv malware on the npm registry | postmortem | 2017-08-02 | 2026-10-15 | https://blog.npmjs.org/post/163723642530/crossenv-malware-on-the-npm-registry.html | A lookalike name (crossenv for cross-env) shipped an env-var exfiltrator for about two weeks; npm removed about 40 related packages. | "about 40" packages; nearly 700 downloads; "at most 50 real installations" (as retrieved) |
| 26 | The Register | This typosquatting attack on npm went undetected for 2 weeks | blog | 2017-08-02 | 2026-10-15 | https://www.theregister.com/2017/08/02/typosquatting_npm/ | Independent account of the same incident; package counts differ by source (37 to 40). | 39 removed and npm's "about 40" (as retrieved) |
| 27 | npm | New package moniker rules | blog | 2017-12-26 | 2026-10-15 | https://blog.npmjs.org/post/168978377570/new-package-moniker-rules | npm's response widened the registration-time equivalence: names differing only in punctuation are now refused, not merged. | "you can no longer publish packages that differ from existing packages in punctuation only." ; example react-native vs reactnative (as retrieved) |
| 28 | Checkmarx | A new stealthier type of Typosquatting attack spotted, targeting NPM | blog | 2023-05 | 2026-10-15 | https://checkmarx.com/blog/a-new-stealthier-type-of-typosquatting-attack-spotted-targeting-npm/ | npm's 2017 lowercase-only rule was not applied to the existing names, leaving 3,815 uppercase packages of which 1,900 could be impersonated by a lowercase twin until npm patched it. | "3,815 packages were found on NPM containing uppercase letters. Out of these, 1,900 packages were at risk of impersonation." (as retrieved) |
| 29 | Dark Reading | Npm Packages Vulnerable to Old-School Weapon: the 'Shift' Key | blog | 2023-05 | 2026-10-15 | https://www.darkreading.com/threat-intelligence/half-of-npm-packages-vulnerable-to-old-school-weapon-the-shift-key | Independent corroboration of the grandfathering gap. | "All the thousands of existing packages with capital letters in their titles remained, though, and no mechanism was implemented to prevent new packages from copying them in all but capitalization." (as retrieved) |
| 30 | Python (PEP 503) | PEP 503, Simple Repository API, Normalized Names | adr | 2015-09-04 | 2026-10-15 | https://github.com/python/peps/blob/main/peps/pep-0503.rst | PyPI's equality is a one-line, ASCII-only regex, and clients must request the normalized URL rather than rely on redirects. | re.sub(r"[-_.]+", "-", name).lower() ; "clients MUST NOT rely on this redirection and MUST request the normalized URL." |
| 31 | Python (PEP 685) | PEP 685, Comparison of extra names | adr | 2022-03-08 | 2026-10-15 | https://github.com/python/peps/blob/main/peps/pep-0685.rst | Seven years later, extras had no agreed equality; pip 22 treated adhoc-ssl and adhoc_ssl as different. The fix reused PEP 503, made writers emit normalized names and error on collisions, and measured 73 clashes (3 among valid names). | "the extra adhoc-ssl was not considered equal to the name adhoc_ssl by pip 22" ; "Tools generating metadata MUST raise an error if a user specified two or more extra names which would normalize to the same name." ; "limited to 73 instances ... only 3 clashes" |
| 32 | Rust | RFC 940, hyphens considered harmful | adr | 2015-03-05 | 2026-10-15 | https://github.com/rust-lang/rfcs/blob/master/text/0940-hyphens-considered-harmful.md | crates.io compares package names case-insensitively and extended equality to treat - and _ as the same, while the compiler sees a separate crate name. | "crates.io compares package names case-insensitively ... you cannot upload a new package named RUSTC-SERIALIZE" ; "we will extend this logic to identify - and _ as well." |
| 33 | Go project | golang.org/x/mod module/module.go, Escaped Paths | source | 2018 | 2026-10-15 | https://github.com/golang/mod/blob/master/module/module.go | Go refuses to let storage decide equality: it encodes case (!a for A) so a case-insensitive filesystem or web server cannot merge rsc.io/QUOTE and rsc.io/quote, and keeps Unicode out until it can define normalization. | "we cannot rely on the file system to keep rsc.io/QUOTE and rsc.io/quote separate. Windows and macOS don't." ; "replace every uppercase letter with an exclamation mark followed by the letter's lowercase equivalent" ; "U+004B ('K'), U+006B ('k'), and U+212A ('K' for Kelvin) are three distinct runes that case-fold to each other." |
| 34 | Discord | Evolving Usernames on Discord | blog | 2023-05 | 2026-10-15 | https://discord.com/blog/usernames | Discord replaced case-sensitive name#discriminator with unique lowercase handles plus a free-form display name; motivated by failed lookups. | "almost half of friend requests fail because of a missing discriminator or the wrong character casing" ; usernames limited to lowercase letters, numbers, period and underscore (as retrieved) |