no-homoglyph-identifiers
Detects homoglyph identifiers and invisible characters that hide what the code actually does
CWE: CWE-1007 OWASP: A08:2021 Software and Data Integrity Failures
Two characters that render identically are still two characters. This rule is part of eslint-plugin-secure-coding.
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-1007 (Insufficient Visual Distinction of Homoglyphs) |
| Severity | Medium (CVSS 5.3) |
| Auto-Fix | ❌ No — only the author knows which character was intended |
| Category | Security |
Vulnerability and Risk
Vulnerability: Source code is reviewed by looking at it. A character that renders as an ASCII letter but is not one, or a character that renders as nothing at all, defeats that review entirely.
const adminRole = 'admin';
const аdminRole = 'guest'; // Cyrillic 'а' (U+0430) — a different binding
const ADMIN_GROUP = 'admin'; // trailing U+200B — a group nobody is ever a member ofRisk: The diff looks correct, the tests that exercise the visible name still pass, and the authorization check reads the impostor. Trojan Source (CVE-2021-42574) is the same class using bidi overrides: the compiler and the reviewer disagree about what the program says, and the compiler wins.
Rule Details
Two narrow detections, deliberately kept apart. They answer different questions, and merging them is what produces the false positives this rule is measured on.
1. Identifiers — script mixing
An identifier is reported when it contains a character that is visually identical to an ASCII Latin letter and also contains ASCII letters. The character set is an explicit list (Cyrillic а е о р с х у і ј ѕ …, Greek ο ν α ρ τ υ …, Armenian, Cherokee, and the fullwidth forms), each mapped to the ASCII letter it impersonates — that mapping is what lets the finding say which letter is being faked.
An identifier written entirely in one non-Latin script — имя, названиеПеременной — is not reported. It is a legitimate non-English identifier: nothing about it is disguised as something else. The attack needs both scripts together, because the disguise only works inside a name the reader already knows in ASCII.
2. Strings — invisible characters only
A string literal or template chunk is reported only for characters that occupy no visible space or reorder what follows: U+00AD, U+180E, U+200B–U+200F, U+202A–U+202E, U+2060–U+2064, U+2066–U+2069, U+FEFF.
Visible non-ASCII text is never reported. Hebrew, Cyrillic, Japanese and emoji in a string are translation data a user reads; there is nothing deceptive about them, and flagging them would tell an i18n bundle it is a vulnerability.
Two further narrowings keep the legitimate uses of these codepoints clean:
- Adjacency. An invisible character is reported only when it sits next to visible ASCII.
U+200Dis a required part of an emoji family sequence and of correct Persian and Hindi text;U+200Cis mandatory in Persian compounds. Between non-ASCII characters the joiner is doing its job. Insideadminit is doing something else. - Raw, not cooked. The rule reads the literal's
rawtext, so a zero-width space written as'admin\u200B'is not reported — the reviewer can see the codepoint and decide. What the rule exists for is the character pasted in as itself, which renders as nothing.
Findings always print the codepoint (U+0430, U+202E). It is the only way to show a character that has no glyph.
Examples
❌ Incorrect
const adminRole = 'admin';
const аdminRole = 'guest'; // U+0430 renders as ASCII "a"
function grantAccess(user) {
return user.role === аdminRole ? 'full-access' : 'read-only';
}
const ADMIN_GROUP = 'admin'; // trailing U+200B, invisible in every editor
class Session {
#аdmin = true; // U+0430 again
}✅ Correct
// Plain ASCII identifiers, one canonical constant.
const ADMIN_ROLE = 'admin';
// Translated UI copy: visible non-ASCII text is data, not deception.
const MESSAGES = {
en: { signIn: 'Sign in', adminBadge: 'Administrator' },
he: { signIn: 'התחברות', adminBadge: 'מנהל מערכת' },
ru: { signIn: 'Войти', adminBadge: 'Администратор' },
ja: { signIn: 'ログイン', adminBadge: '管理者' },
};
// A deliberate zero-width character, written so a reviewer can see it.
const ZERO_WIDTH_SPACE = '\u200B';Configuration
{
rules: {
'secure-coding/no-homoglyph-identifiers': ['error', {
checkIdentifiers: true,
checkStrings: true
}]
}
}Options
| Option | Type | Default | Description |
|---|---|---|---|
checkIdentifiers | boolean | true | Check identifier names for script-mixing homoglyphs |
checkStrings | boolean | true | Check string literals and template chunks for invisible characters |
Error Message Format
🔒 CWE-1007 CVSS:5.3 | Identifier "аdminRole" mixes scripts: U+0430 renders as ASCII "a" but is a different character | MEDIUM
Fix: Rewrite the identifier in ASCII, or confirm the binding it resolves to is the one you intended | https://cwe.mitre.org/data/definitions/1007.html🔒 CWE-1007 CVSS:5.3 | String contains invisible character U+200B at index 6 - the text is not what it appears to be | MEDIUM
Fix: Remove the character, or write it as an escape (\u200B) so it is visible in review | https://trojansource.codes/Known False Negatives
Comments
Why: The rule visits identifiers, string literals and template chunks. A bidi override inside a comment — the original Trojan Source demonstration — is not an AST value it reads.
Mitigation: Enforce a repository-wide scan for bidi controls in source bytes (many hosts, including GitHub, now warn on them).
Invisible characters written as escapes
Why: Deliberate, see Raw, not cooked above. An escape is visible in review.
Mitigation: None needed — that is the recommended way to write one.
A single confusable character surrounded only by non-ASCII
Why: The identifier check requires an ASCII letter in the same name, and the string check requires a visible ASCII neighbour. A name or string entirely in a non-Latin script is treated as legitimate text.
Mitigation: Accepted. Reporting it would flag every non-English identifier and every i18n bundle in the repository.
Further Reading
- CWE-1007 — Insufficient Visual Distinction of Homoglyphs Presented to User
- Trojan Source — CVE-2021-42574 / CVE-2021-42694
- Unicode TR39: Security Mechanisms — confusable detection and mixed-script restriction
Related Rules
no-insecure-comparison— the comparison an invisible character silently defeats
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.