三类绕过手法
第一类是形式差异绕过:同一可见内容存在规范或兼容等价的多种码点写法,过滤器只识别其中一种。第二类是视觉相似字符:用西里尔字母、希腊字母或相似形状字符伪装成标准字符,构造看起来完全相同的账号名或域名。第三类是控制与格式字符:零宽字符、双向控制符插入后肉眼不可见,却会改变比较与显示顺序,用于隐藏恶意内容或破坏审计。这三类都能用「先归一化、再校验、再使用」的顺序加上显式字符白名单来约束。
基本信息
| 根因 | 校验对象与最终使用对象不一致 |
|---|---|
| 手法一 | 规范或兼容等价的形式差异(不同码点,同形) |
| 手法二 | 视觉相似字符(不同脚本的相似字形) |
| 手法三 | 不可见控制与格式字符 |
| 防御顺序 | 归一化 → 字符白名单/黑名单校验 → 使用 |
| 配套手段 | 唯一约束用折叠值、显式拒绝格式字符、日志记录原始码点 |
典型使用场景
- 账号体系:防止同形不同码点的用户名并存,避免钓鱼式冒充
- 域名与链接校验:识别视觉相似域名与不可见字符拼接的钓鱼链接
- 内容风控:黑名单匹配前折叠兼容字符,避免用全角写法绕过
- 日志审计:不可见字符会破坏日志结构与检索,需要在入口清洗
- 文件命名:跨系统同名文件因归一化形式不同被当成两个文件
校验顺序示例
校验顺序示例
# 错误顺序:先校验后归一化,攻击者可用等价形式绕过
python -c "import unicodedata as u; s='\uff21dmin'; print('blocked' if 'admin' in s else 'allowed')" # 全角 A 绕过
# 正确顺序:先兼容折叠再校验
python -c "import unicodedata as u; s=u.normalize('NFKC','\uff21dmin'); print('blocked' if 'admin' in s else 'allowed')"
# 检测不可见格式字符(Cf 类)
python -c "import unicodedata as u; s='us\u200ber'; print([hex(ord(c)) for c in s if u.category(c)=='Cf'])"
# 唯一性判断使用折叠后的值,展示仍用原值
python -c "import unicodedata as u; print(u.normalize('NFKC','\uff41pple') == u.normalize('NFKC','apple'))"风险与对策
| 风险 | 表现 | 对策 |
|---|---|---|
| 等价形式绕过 | 过滤规则被另一种码点写法跳过 | 先归一化(含兼容)再校验 |
| 同形账号冒充 | 两个账号肉眼无法区分 | 唯一性用折叠值,必要时限制可用脚本 |
| 不可见字符注入 | 内容比较与显示顺序异常 | 拒绝或剥离格式字符,日志记录码点 |
| 钓鱼域名 | 域名与正牌仅差相似字符 | 域名展示与校验使用 punycode 与相似性检测 |
| 文件名冲突 | 同一文件被系统视为两个 | 统一归一化形式后再做去重与同步 |
「看起来一样」不能作为安全判据:人眼无法分辨同形字符与不可见字符。所有涉及身份、权限与唯一性的判断都必须基于归一化并显式校验后的值,而不是原始输入。
限制字符集是最有效的防御之一:账号名、标识符这类不需要复杂文字的场景,直接限定可用字符范围(字母数字加少量符号),可以一次性消掉大部分同形与不可见字符风险,代价是对多语言用户不够友好,需要与业务权衡。
排查可疑输入时按码点打印是基本功:把字符串转成 U+XXXX 列表,同形字符与零宽字符会立刻暴露。这一步应作为风控与审计工具的常规能力,而不是临时手工操作。