Unicode Confusables Checker
Check whether a string is a Unicode homoglyph of another. Per-character codepoints, scripts and Identifier_Status, the UTS #39 skeleton, restriction level, mixed-script detection and the punycode an IDN label produces.
Unicode Confusables Checker
Comparison
Two strings are confusable when their skeletons are equal.Every character in the first string
| Glyph | Code point | Name | Script | Identifier status | Prototype |
|---|
Punycode (RFC 3492)
Whatxn-- form the first string produces as a domain.
Skeleton vs NFKC — neither contains the other
A defence built on one has a hole shaped like the other.Only the skeleton catches these
Separate letters that happen to be drawn alike. NFKC leaves them untouched.
Only NFKC catches these
Compatibility variants with no confusables row — fullwidth digits are the standing example.
Both catch these
The overlap, which is smaller than people assume.
Unicode 17.0.0
UTS #39 states that skeletons “are not stable across versions of Unicode… If they are stored, they
must be recomputed.” This page recomputes on every keystroke and stores nothing. Between Unicode
16.0.0 and 17.0.0, 210 mappings were added and 39 were retargeted.
Everything runs in your browser — the string you paste is never transmitted.
How to use the Unicode confusables checker
A domain label, a package name, a username, a filename — anything that is meant to be an identifier. Paste it whole; the checker walks it code point by code point.
The skeleton replaces every character with its prototype. If a Cyrillic а comes back as a Latin a, that letter is not what it appeared to be.
Two strings are confusable when their skeletons are identical. The comparison panel aligns them and marks exactly which characters differ.
The punycode panel shows the xn-- form a registry stores. The table names every character, its script and whether it is even allowed in an identifier.
Homoglyphs, skeletons, and what a confusables check actually proves
A homoglyph attack does not break anything. It relies on the fact that Unicode contains many characters that are drawn the same way and are not the same character. Cyrillic а, Greek ο, Cherokee Ꮮ and Latin a, o and L are separate letters with separate code points, separate scripts and, in most typefaces, identical shapes. A string built from them reads as one word to a person and compares as a different word to a computer. That gap is the whole attack, and it exists wherever a human being reads an identifier and trusts it: domain names, package names, usernames, filenames, email display names.
The skeleton, and why it must never be stored
Unicode's answer is in UTS #39, Unicode Security Mechanisms. It publishes a table mapping each confusable character to a prototype — an exemplar for the class of characters that look like it — and defines a skeleton operation that decomposes a string, drops the invisible characters, replaces each remaining character with its prototype, and recomposes. Two strings are confusable when their skeletons are equal. It is a deliberately blunt instrument: it imposes transitivity, so it is sometimes over-inclusive, and it is not a similarity score.
The part that gets missed is that the table moves. This tool is pinned to Unicode 17.0.0, whose
confusables file carries 6,565 mappings; Unicode 16.0.0 carried 6,355. Two hundred and ten mappings were
added and thirty-nine were repointed at a different prototype. One of the thirty-nine is U+04CF CYRILLIC
SMALL LETTER PALOCHKA, whose prototype changed from Latin i to Latin l — and the palochka is a character
in the most-cited lookalike domain ever registered. Under Unicode 16.0.0 that label's skeleton was
appie. Under 17.0.0 it is apple. The standard says so plainly: skeletons are
not stable across versions and must be recomputed on upgrade. A cached skeleton column in a database is
a comparison against last year's idea of what looks alike.
Eight of the 6,565 confusable sources are plain ASCII. 0 maps to O, 1 and I and the pipe all map to l, and m maps to the two-character sequence rn — which is why the same algorithm built for cross-script attacks catches the oldest typosquat in the book.
Normalisation is not a substitute, in either direction
The common first instinct is Unicode normalisation — NFKC — which folds fullwidth letters, ligatures and mathematical alphabets back toward plain ASCII. It does not touch the cases that matter here. Cyrillic а, е, р and с pass through NFKC completely unchanged, because they are not compatibility variants of anything: they are separate letters that happen to be drawn identically. A reverse gap exists too. All ten fullwidth digits, U+FF10 through U+FF19, fold under NFKC and have no confusables row at all — and the letters are only partly covered in the other direction: 31 of the 52 fullwidth ASCII letters carry a row and 21 do not. Neither transform contains the other, and neither is complete on its own, which is why the panel above splits your input into three columns rather than claiming one check is enough.
Restriction levels, and the case they cannot catch
UTS #39 also defines six restriction levels, from ASCII-Only up to Unrestricted, based on which scripts a string draws on. They are useful and they are frequently oversold. Mixing one Cyrillic letter into a Latin word produces a mixed-script string that the levels catch immediately. A label written entirely in Cyrillic does not: it is perfectly consistent, it qualifies as Single Script, and it passes every level except ASCII-Only. The standard's own worked example is a Cyrillic string that reads as “Circle”. Restriction levels ask whether a string is internally consistent. A well-built impersonation always is. That is why this tool reports the level and the skeleton side by side, and flags the combination — single-script and full of remapped characters — rather than letting a clean level read as a clean bill of health.
One more reading trap worth naming: a level of Unrestricted usually means nothing sinister. The algorithm's first test is whether every character is even permitted in an identifier, and ordinary punctuation is not. Type a sentence with a space in it and the answer is Unrestricted, because a space cannot appear in an identifier — not because the sentence is dangerous. The tool names the offending characters rather than leaving you to guess.
Why this matters more in a multi-script region
Southeast Asia is one of the most script-dense regions on the internet. Thai, Khmer, Lao, Burmese,
Tamil, Chinese, Jawi-script Malay and Latin-with-diacritics Vietnamese are all in daily commercial use,
frequently on the same page as English. The IANA root zone currently delegates 1,438 top-level domains,
of which 151 are internationalised xn-- labels — and ASEAN is well represented among them:
.ไทย for Thailand, .新加坡 and .சிங்கப்பூர் for Singapore, and
.مليسيا for Malaysia are all live. Registries that permit non-Latin labels are doing the
right thing for the people who read those languages, and the same policy widens the space a lookalike
can hide in. A Singapore or Malaysian brand whose name is legitimately written in two scripts cannot be
protected by a rule that simply forbids script mixing; the honest defence is to compare skeletons
against the names you actually own, and to look at what a label decodes to rather than at what it
renders as.
What this tool does not do
It does not detect Trojan Source (CVE-2021-42574). That is a different attack: bidirectional control characters that make source code render in one order and compile in another. Nothing in it looks like anything else — the deception is positional, not visual. It also computes the pre-bidi form of the skeleton, which UTS #39 equates to the full definition for any string without a strong right-to-left character; when your input contains one, the page says so rather than presenting a value it has not earned. And the punycode panel shows the form a label produces, not whether a registry would accept it: that needs the full UTS #46 mapping and validity rules, which are a separate body of data.
10 facts about Unicode confusables
Unicode 17.0.0 publishes 6,565 confusable mappings. Unicode 16.0.0 published 6,355 — 210 were added, none removed, and 39 existing mappings were repointed at a different prototype.
Eight of the confusable sources are plain ASCII characters: 0 → O, 1 → l, I → l, | → l, m → rn, " → '', ` → ' and % → º/₀. Package registries that reject non-ASCII names are still exposed to these.
U+04CF CYRILLIC SMALL LETTER PALOCHKA had the prototype Latin i in Unicode 16.0.0 and Latin l in 17.0.0. It appears in the 2017 apple.com demonstration label, whose skeleton therefore changed from "appie" to "apple".
The prototype of m is the two-character sequence rn. Mappings are not one-to-one: 6,565 source characters map onto just 2,244 distinct prototype code points.
Cyrillic а, е, р and с survive NFKC normalisation completely unchanged. They are not compatibility variants of the Latin letters — they are separate letters that happen to be drawn the same way.
All ten fullwidth digits U+FF10–U+FF19 fold to ASCII under NFKC and none of them appears in confusables.txt. The letters are patchy in the other direction: 31 of the 52 fullwidth ASCII letters carry a confusables row and 21 do not. Neither transform contains the other, and the skeleton’s own coverage is uneven.
IdentifierStatus.txt marks 33,791 code points as Allowed in identifiers. Everything else — including the space character — is Restricted, which is why an ordinary sentence scores Unrestricted.
A string written entirely in one non-Latin script passes every restriction level except ASCII-Only, no matter how convincingly it impersonates a Latin word. UTS #39's own example is a Cyrillic string reading "Circle".
Since the bidirectional update, UTS #39 defines skeleton(X) as bidiSkeleton(LTR, X) — the Unicode Bidirectional Algorithm runs before the mapping. For any string with no strong right-to-left character the standard states the two are identical.
The IANA root zone currently delegates 1,438 top-level domains, 151 of them internationalised xn-- labels — including .ไทย, .新加坡, .சிங்கப்பூர் and .مليسيا.
Frequently asked questions
-
A character that is drawn like another character but is a different code point — Cyrillic а against Latin a, Greek ο against Latin o. Unicode publishes the list in UTS #39, and this tool is pinned to the Unicode 17.0.0 edition of it.
-
UTS #39's canonical form for comparison. The string is decomposed, characters with no visible glyph are dropped, every remaining character is replaced by its prototype, and the result is decomposed again. Two strings are confusable exactly when their skeletons match.
-
Because the mapping table changes. 210 mappings were added between Unicode 16.0.0 and 17.0.0 and 39 were retargeted, and one of the 39 changed the skeleton of the most famous lookalike domain there is. The standard says explicitly that skeletons must not be stored across a version upgrade. This page recomputes on every keystroke.
-
No, and it is not a subset either. Cyrillic а, е, р and с pass through NFKC unchanged, so normalisation misses the cross-script cases entirely. Meanwhile fullwidth digits fold under NFKC and have no confusables row, so the skeleton misses those. The panel above splits your input into three columns to show both gaps.
-
Because restriction levels describe identifiers, not prose. The algorithm's first test is whether every character is permitted in an identifier, and a space is not. The tool names the characters responsible so the verdict cannot be misread as a warning about the string.
-
Not necessarily. A label written entirely in Cyrillic is perfectly consistent and passes every level except ASCII-Only while still being skeleton-identical to a Latin word. That is the whole-script confusable case, and restriction levels are structurally unable to catch it. The tool flags the combination when it sees it.
-
No. Trojan Source (CVE-2021-42574) uses bidirectional control characters to make source code render in a different order from the one the compiler reads. It is a positional attack, not a visual one, and needs different tooling.
-
UTS #39 defines skeleton(X) as bidiSkeleton(LTR, X), which runs the Unicode Bidirectional Algorithm first. The standard names a fast path: for a string with no Bidi_Class R or AL character the two are identical. This tool computes that form and tells you when your input falls outside the fast path rather than presenting a value it has not earned.
-
It shows what A-label — the xn-- form — a name produces under RFC 3492, after lowercasing and NFC. That is the form a registry stores and the form a browser may display in place of the letters. It is not a full UTS #46 validity check, so it does not tell you whether a registry would accept the label.
-
No. The Unicode data file and the algorithm both run in your browser; the tool makes no network request with your input and stores nothing. You can check by pasting with your network disconnected.
Related News
You may be interested in these recent stories from our newsroom.
-
The Stop Rogue AI Act Would Make Companies Inventory Their AI Agents. It Is Voluntary for Almost Everyone.
The Stop Rogue AI Act binds only federal contractors bidding new work, and gives NIST a year from enactment. It also asks operators to inven...
-
CISA Added Seven Exploited Flaws. Five Due in Three Days, Two in Fourteen.
Five carry a three-day federal clock, two carry fourteen days, in a single batch. Three of the seven sit in AI and build infrastructure: Lit...
-
ServiceNow's Exploited Endpoint Was Not Broken. Its Auth Flag Was False.
A Scripted REST API had its authentication flag set false, so anonymous requests ran as Guest. Exploited 2 to 3 June, hotfixed on the 5th, d...
Method & sources
How it computes
UTS #39 §4 skeleton: NFD, drop Default_Ignorable_Code_Point characters, replace each character with its prototype from confusables.txt, reapply NFD. Two strings are confusable when their skeletons are equal. Mixed-script detection is §5.1 (augmented script sets intersected across the string); restriction levels are §5.2. Punycode is RFC 3492. Data is Unicode 17.0.0, vendored and recomputed per keystroke.
What this tool implements
- The skeleton is UTS #39 §4 exactly: NFD, remove Default_Ignorable_Code_Point characters, map to prototypes, reapply NFD. Each of those four steps is bound by its own test, because three of them can be deleted without changing any verdict the page displays.
- Confusability is reported BOTH folded and unfolded. Eight confusable sources are ASCII and one of them is `0 → O`, a capital: skeleton("g00gle") is "gOOgle", so folding only the input returns "not confusable" for the oldest typosquat there is. Folding the skeletons catches it.
- ⚠️ NOTHING IS CACHED. UTS #39 states that skeletons "are not stable across versions of Unicode… If they are stored, they must be recomputed." 39 mappings were retargeted between 16.0.0 and 17.0.0, one of which changed the skeleton of the 2017 apple.com label from "appie" to "apple". The page recomputes on every keystroke and the Unicode version is displayed.
- ⚠️ THE SKELETON SHOWN IS internalSkeleton. UTS #39 defines skeleton(X) as bidiSkeleton(LTR, X), which runs the Unicode Bidirectional Algorithm first, and the standard names the fast path this tool takes: for a string with no Bidi_Class R or AL character the two are identical. For a string containing one, the page says so rather than presenting a value it has not earned.
- ⚠️ A restriction level of "Unrestricted" is usually not a warning. §5.2 tests the identifier profile before anything else, so any string containing a space lands there — the tool names the responsible characters instead of leaving the reader to infer danger.
- ⚠️ Restriction levels cannot catch a whole-script confusable, and the page flags that combination explicitly. A label written entirely in Cyrillic is internally consistent, qualifies as Single Script, and can still be skeleton-identical to a Latin word.
Sources
- Unicode Technical Standard #39, Unicode Security Mechanisms: https://www.unicode.org/reports/tr39/ — §4 defines the skeleton and the confusable relation, §5.1 mixed-script detection and augmented script sets, §5.2 the six restriction levels. Every algorithm on the page is from this document.
- confusables.txt, Unicode 17.0.0, dated 2025-07-22: https://www.unicode.org/Public/security/latest/confusables.txt — 6,565 mappings, of which 8 have plain-ASCII sources.
- IdentifierStatus.txt and IdentifierType.txt, Unicode 17.0.0: https://www.unicode.org/Public/security/latest/ — 33,791 code points are Identifier_Status=Allowed; IdentifierType supplies the reason a character is Restricted.
- Unicode Standard Annex #24, Unicode Script Property, with Scripts.txt and ScriptExtensions.txt: https://www.unicode.org/reports/tr24/ — Script_Extensions, not Script, is what §5.1 requires.
- Unicode Standard Annex #31, Identifiers and Syntax, Table 5 Recommended Scripts: https://www.unicode.org/reports/tr31/ — the 28-script list §5.2 tests against for Moderately Restrictive.
- RFC 3492, Punycode: A Bootstring encoding of Unicode for IDNA: https://www.rfc-editor.org/rfc/rfc3492 — the encoder is checked against all seven of its §7.1 sample strings.
- IANA root zone TLD list: https://data.iana.org/TLD/tlds-alpha-by-domain.txt — the 1,438 / 151 IDN figures quoted on the page were counted from this file on 2026-09-02.
Abridged — the full review record for this tool runs longer than the list above.
Pick up where you left off
Stored only in this browser — never sent to our servers.