Confusable Detection

Unicode confusables (homoglyphs) are characters from different scripts that look visually identical or very similar. For example, Cyrillic "а" (U+0430) looks like Latin "a" (U+0061). Attackers exploit this for phishing, impersonation, and spoofing.

disarm implements Unicode TR39 confusable detection and normalization with multi-target script support, auto-generated from the official Unicode TR39 confusables.txt (version 17.0.0). The tables cover Cyrillic, Greek, Armenian, Georgian, CJK compatibility, mathematical symbols, fullwidth forms, and other visually confusable characters. Mappings are based on visual similarity, not phonetic equivalence.

Detecting confusables

from disarm import is_confusable, is_mixed_script

# Cyrillic Н looks like Latin H
assert is_confusable("Неllo") == True
assert is_mixed_script("Неllo") == True

# Pure Latin — no confusables
assert is_confusable("Hello") == False
assert is_mixed_script("Hello") == False

Normalizing confusables

Replace confusable characters with their target-script equivalents:

from disarm import normalize_confusables

# Cyrillic а, е, о → Latin a, e, o
assert normalize_confusables("Неllo Wоrld") == 'Hello World'

# Greek omicron → Latin o
assert normalize_confusables("Ηellο") == 'Hello'

Target script

By default, confusables are normalized to Latin. You can specify a different target script to normalize towards that script instead:

# Normalize to Latin (default) — non-Latin homoglyphs → Latin
assert normalize_confusables("раypal") == 'paypal'

# Normalize to Cyrillic — non-Cyrillic homoglyphs → Cyrillic
assert normalize_confusables("paypal", target_script="cyrillic") == 'раураӏ'

Supported target scripts

Target Mappings Description
"latin" (default) ~2,063 Non-Latin → Latin. Cyrillic а→a, Greek Ρ→P, etc.
"cyrillic" ~1,369 Non-Cyrillic → Cyrillic. Latin A→А, p→р, etc.

Characters without a confusable equivalent in the target script pass through unchanged. This is pure visual mapping — not transliteration. Latin f has no Cyrillic lookalike, so it stays as f.

Script detection

Identify which Unicode scripts are present in a string:

from disarm import detect_scripts, Script

scripts = detect_scripts("Hello Мир")
assert scripts == [Script.LATIN, Script.CYRILLIC]

scripts = detect_scripts("東京 Tokyo")
assert scripts == [Script.HAN, Script.LATIN]

The Script enum

Script enumerates the 39 Unicode scripts disarm recognizes:

Major world scripts:

Script Example characters
LATIN A–Z, a–z, À–ÿ
CYRILLIC А–Я, а–я
GREEK Α–Ω, α–ω
ARABIC ع, ب, ت
HEBREW א, ב, ג

Indic scripts:

Script Example characters
DEVANAGARI अ, आ, इ
BENGALI অ, আ, ই
GURMUKHI ਅ, ਆ, ਇ
GUJARATI અ, આ, ઇ
ORIYA ଅ, ଆ, ଇ
TAMIL அ, ஆ, இ
TELUGU అ, ఆ, ఇ
KANNADA ಅ, ಆ, ಇ
MALAYALAM അ, ആ, ഇ
SINHALA අ, ආ, ඇ

East Asian scripts:

Script Example characters
HAN 中, 文, 字
HIRAGANA あ, い, う
KATAKANA ア, イ, ウ
HANGUL 가, 나, 다

Southeast Asian scripts:

Script Example characters
THAI ก, ข, ค
LAO ກ, ຂ, ຄ
MYANMAR က, ခ, ဂ
KHMER ក, ខ, គ
BALINESE ᬅ, ᬆ, ᬇ
JAVANESE ꦄ, ꦆ, ꦈ
TAI_LE ᥐ, ᥑ, ᥒ
NEW_TAI_LUE ᦀ, ᦁ, ᦂ

Central/North Asian scripts:

Script Example characters
TIBETAN ཀ, ཁ, ག
MONGOLIAN ᠠ, ᠡ, ᠢ

Caucasian scripts:

Script Example characters
GEORGIAN ა, ბ, გ
ARMENIAN Ա, Բ, Գ

African scripts:

Script Example characters
ETHIOPIC ሀ, ለ, ሐ
NKO ߊ, ߋ, ߌ
VAI ꔀ, ꔁ, ꔂ

Middle Eastern scripts:

Script Example characters
SYRIAC ܐ, ܒ, ܓ
THAANA ހ, ށ, ނ
COPTIC Ⲁ, Ⲃ, Ⲅ

Americas:

Script Example characters
CHEROKEE Ꭰ, Ꭱ, Ꭲ
CANADIAN_ABORIGINAL ᐁ, ᐂ, ᐃ

Historical European scripts:

Script Example characters
RUNIC ᚠ, ᚡ, ᚢ
OGHAM ᚁ, ᚂ, ᚃ

Meta-scripts:

Script Description
COMMON Digits, punctuation, whitespace
INHERITED Combining diacritical marks

Use cases

Anti-phishing

Detect domain names that use mixed scripts to impersonate legitimate sites:

from disarm import is_mixed_script, normalize_confusables

# Detect Latin homoglyphs in a "Cyrillic" domain
domain = "аpple.com"  # first "a" is Cyrillic
if is_mixed_script(domain):
    normalized = normalize_confusables(domain)
    print(f"Suspicious: looks like {normalized}")

# Detect Cyrillic homoglyphs injected into Russian text
text = "Банк pоссии"  # Latin 'p' and 'o' instead of Cyrillic
normalized = normalize_confusables(text, target_script="cyrillic")
assert normalized == 'Банк россии'

Username validation

Ensure usernames don't contain confusable characters:

from disarm import is_confusable

def validate_username(name: str) -> bool:
    if is_confusable(name):
        raise ValueError("Username contains confusable characters")
    return True

Search normalization

Normalize confusables before indexing for search:

from disarm import TextPipeline

index_pipeline = TextPipeline(
    normalize="NFKC",
    confusables=True,
    fold_case=True,
)