← 中文资源

简体中文版本

检测同形异义混合文字名称

识别用户名中混用拉丁字母与西里尔、希腊字母的同形字符,并逐个说明可疑字符的 Unicode 名称。

阅读英文原文

功能说明

Unicode 中不同文字系统存在形状几乎相同的字符,例如西里尔字母 а 与小写拉丁 a、希腊字母 ο 与拉丁 o。攻击者可以把用户名中的拉丁字母替换成西里尔字母,注册一个肉眼难以分辨的账号,用于冒充客服、品牌或同事。仅做 Unicode 规范化无法解决,因为 NFKC 不会把西里尔字母映射成拉丁字母。

本例把字母按码点区间归类为拉丁、西里尔、希腊或其他,统计一个字符串中出现的文字系统。纯拉丁名称正常;只要同时出现拉丁和另一种文字,就标记为可疑。explain 进一步列出每个非拉丁字母的字符、Unicode 名称和所属文字,便于人工复核。

关键代码

SCRIPT_RANGES 用闭区间描述三类字母的主要码点范围,拉丁范围包含扩展区,能覆盖常见带音调字母。script_of 遍历范围返回文字名称,不在表中的字符归为其他。find_mixed_scripts 只统计字母,去掉数字、标点和表情,再排序返回。is_suspicious 要求同时包含拉丁且文字种类大于一,避免把纯中文或纯俄文名称误判。explain 用列表推导筛出非拉丁字母,并调用 unicodedata.name 取得可读名称。

使用注意

码点区间是粗略分类,某些字母会落在多个文字的共享区,实际部署应使用 Unicode Script 属性或专门的安全库。该函数只能提示风险,不能证明恶意;国际化产品中混合文字可能是正常需求。还应对标识符做 NFKC 规范化、限制方向控制符和零宽字符,并对域名使用 punycode 检查。最稳妥的策略是注册时限制允许的文字集合,或对高价值账号启用人工审核。

验证方式

运行脚本,paypal 与 admin 只包含拉丁字母,is_suspicious 为 False;pаypal 中第二个字母是西里尔 а,因此文字列表包含 CYRILLIC 与 LATIN,结果为 True。explain 输出该字符的 Unicode 名称 CYRILLIC SMALL LETTER A,证明同形字符被准确定位。

代码示例

import unicodedata

SCRIPT_RANGES = {
    "LATIN": [(0x0041, 0x024F), (0x1E00, 0x1EFF)],
    "CYRILLIC": [(0x0400, 0x04FF)],
    "GREEK": [(0x0370, 0x03FF)],
}


def script_of(char: str) -> str:
    code = ord(char)
    for name, ranges in SCRIPT_RANGES.items():
        if any(start <= code <= end for start, end in ranges):
            return name
    return "OTHER"


def find_mixed_scripts(text: str) -> list[str]:
    scripts = {script_of(char) for char in text if char.isalpha()}
    return sorted(scripts - {"OTHER"})


def is_suspicious(text: str) -> bool:
    scripts = find_mixed_scripts(text)
    return "LATIN" in scripts and len(scripts) > 1


def explain(text: str) -> list[dict[str, str]]:
    return [
        {
            "char": char,
            "name": unicodedata.name(char, "UNKNOWN"),
            "script": script_of(char),
        }
        for char in text
        if char.isalpha() and script_of(char) not in {"LATIN", "OTHER"}
    ]


if __name__ == "__main__":
    for sample in ["paypal", "pаypal", "admin"]:
        print(sample, find_mixed_scripts(sample), is_suspicious(sample))
    print(explain("pаypal"))