How Homoglyphs Can Be Misused in Python Code¶
Homoglyphs introduce security risks in several scenarios. They are a proven technique for hiding malicious code that is difficult for both humans and many automated tools to detect.
Typosquatting / supply-chain attacks¶
A malicious package named reqυests (using a Greek upsilon υ instead of the Latin u) can be published to PyPI. Victims who mistype the legitimate package name requests may install and import the malicious version.
Backdooring code¶
Two functions that look identical can coexist because Python treats them as different names:
def transfer(amount): # ASCII 'a'
return real_transfer(amount)
def trаnsfer(amount): # Cyrillic 'а' — different identifier!
exfiltrate(amount) # backdoorBypassing security checks¶
Regex filters and string comparisons that assume ASCII can be trivially bypassed:
>>> "раyment" == "payment"
False
>>> "раyment".isascii()
FalseMisuse of built-ins such as exec¶
The following call works because Python applies NFKC normalisation to identifiers:
ℯ𝓍ℯ𝒸("print(2 + 2)") # becomes exec after NFKC normalisationPython normalises identifiers with NFKC at parse time, so ℯ𝓍ℯ𝒸 is treated as exec. However, the characters in the source are not equal to the literal string "exec", which can fool many static-analysis (SAST) tools.
What is NFKC normalisation?
NFKC stands for Normalization Form Compatibility Composition.
Characters are first decomposed by compatibility equivalence, then recomposed by canonical equivalence.
See Unicode equivalence and the authoritative reference Unicode Normalization Forms (UAX #15).
Consequently, the following also executes successfully and is frequently missed by SAST scanners:
ℯ𝓍ℯ𝒸("import os; os.system('rm -rf ~/.*')")Defensive Tips¶
To protect your codebase from homoglyph attacks and unintended identifier spoofing, implement the following defensive practices:
Enforce ASCII-Only Identifiers: PEP 8 recommends restricting identifiers to ASCII characters. You can enforce this automatically using modern linters (such as Ruff, Flake8, or Pylint) or with a simple pre-commit hook or CI check that rejects non-ASCII characters in source files.
Validate Dynamic Identifiers: If your application accepts or constructs identifiers dynamically (such as through user input, plugins, or reflection), validate them immediately using
str.isascii()before execution or lookup.Detect Normalization Anomalies: Use Python’s
unicodedatamodule to check if an identifier changes under NFKC normalization or deviates from standard ASCII:
import unicodedata
def is_safe_identifier(identifier: str) -> bool:
if identifier.isascii():
return True
# Flag identifiers that change under NFKC normalization
normalized = unicodedata.normalize("NFKC", identifier)
if normalized != identifier:
return False
return TrueConfigure Review Tooling and SAST: Ensure that your code-review interfaces, IDEs, and static analysis (SAST) scanners are configured to explicitly highlight non-ASCII characters, zero-width characters, and mixed-script identifiers so reviewers can catch obfuscated code.
