Evidence ledger 31 sources Checked 15 Oct 2026

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)