第 8 讲 / 共 10 讲

看不见的字符与看得见的风险:Unicode 文本安全入门

两段用户名看上去完全一样,系统却说不同;复制来的编号明明正确,搜索仍然没有结果;一个文件名的显示顺序又让人产生误解。这些现象不一定是攻击,也不一定是程序出错。Unicode 允许文字表达超出可见字形的信息,安全处理的关键是把这些信息显露出来,再根据字段用途作出判断。

Copy2Paste约 11 分钟阅读

看不见,并不等于没有用途

有些字符自身不画出明显图形,却能影响连接、分隔、断行或方向。零宽连接符可以参与特定 emoji 序列,也用于文字排版;其他控制信息则帮助混合方向的文字保持可读。这些控制信息虽然不单独显形,却仍参与表达;把它们统统当成垃圾删除,容易破坏本来正确的文本。

比如一条包含家庭 emoji 的消息,删除连接符后可能变成几个独立图案。对于另一种语言,删除某个连接控制符甚至可能改变正常的书写形式。清理工具应说明将处理哪些字符,并允许查看结果。用户想删除的是意外混入的内容,通常不是文本中所有肉眼看不见的结构。

安全工作的起点因此不是一条全局删除规则,而是用途说明。文章正文、显示名称、账户标识、机器编号和源代码,需要不同策略。先写清字段要表达什么,再确定什么字符不符合预期。这样的限制才能解释给用户,也更容易通过测试证明它没有误伤合法输入。

先让隐藏信息可见

发现编号无法匹配时,先把原文另存一份,再查看每个码位的位置、名称和类别。例如用 U+200B 这样的标签标出零宽空格,比在页面上额外画一个普通空格更准确。后者会暗示原文包含可见间隔,而标签明确告诉读者这里存在一个具体字符。

检查视图应与实际内容区分开。可以在诊断面板里显示转义形式,但复制原文的按钮仍应复制原始文本;复制诊断结果则应明确标注。否则用户可能把“用于解释字符的文字”当成原字符粘贴回业务系统,又制造第二个问题。工具应该帮助定位差异,而不是悄悄换掉被分析的对象。

一个简单练习是比较 AB12 与在 B 后面插入零宽字符的版本。视觉上可能几乎相同,但码位清单会多一项。接着分别测试精确比较、搜索和粘贴,记录具体行为。这里不应推导出所有搜索都必然失败,因为不同软件可能有不同匹配规则;实验价值在于确定你的系统实际做了什么。

检查位置时还应说明计数方法。如果工具按码位编号,另一款编辑器按码元定位,包含补充平面字符的文本可能出现位置差异。把可疑字符前后的一小段上下文与码位一起显示,更便于用户找到对应位置。诊断界面可以解释这些单位,但不必把所有内部细节塞进普通输入表单,避免用户为修正一个编号而先学习整套编码术语。

逻辑顺序与屏幕顺序可以不同

阿拉伯文、希伯来文与数字、英文混排时,需要双向文本算法安排屏幕顺序。内存中的逻辑顺序与显示顺序因此可能不同,方向控制字符还能影响布局。这是正常语言支持的一部分。但在文件名、代码和日志等需要精确判断字符顺序的场合,显示与逻辑之间的差距也可能误导读者。

防护重点是让关键界面提供可靠的检查方式。代码审查可以标示方向控制符,日志查看器可以同时提供转义文本,文件管理界面可以结合明确的类型信息,而不把最后看见的几个字母当成唯一依据。不要为了演示风险就在正文里埋入控制符;使用码位名称和可见说明,同样能解释机制。

在网页中组织混合方向文本时,优先采用合适的方向标记与隔离结构。结构化页面能够清楚地表达某一片段的方向,维护者也能在源代码里看见边界。对于只能使用纯文本的场景,再根据需要使用方向控制。把界面显示隔离与底层存储顺序分别测试,可以避免修好一处却影响相邻文字。

审查记录也不应只保存截图。截图适合说明读者看到的顺序,转义后的原文适合说明实际序列,两者放在一起才便于复核。在自动检测发出提醒时,维护者应查看控制符的范围和上下文,判断它是在表达合法语言方向,还是造成不必要的视觉误导。提醒的目标是促成检查,而不是让所有含控制符的内容自动失去资格。

形似字符与隐藏字符是两类问题

一个字母即使完全可见,也可能与另一种文字的字母很像。拉丁小写 a 与西里尔小写 а 就是常见示意:它们属于不同字符,某些字体下外观接近。相似程度会随字体变化,同一文字内部也存在容易混淆的字符,因此不能简单把风险等同于“混用了两种文字”。

Unicode 的安全机制提供形似检测等工具,但检测结果不是恶意意图的证明。真实人名、借词、技术名称都可能混合文字。对于公开账户名,可以结合脚本限制、相似名称提醒和人工处理机制;对于正文,强行拒绝所有混合文字会妨碍正常交流。风险政策必须对应业务场景,而不是把语言本身视为异常。

假设系统已经有一个认证机构账户,新注册名称与它高度相似。合理处理可以是提示注册者修改,并提供申诉渠道;同时在界面明确展示账户的稳定标识和认证状态。只改变显示字体无法替代账户模型,只计算相似度也无法判断所有冒用。多种信息应共同支持用户识别,而不是把责任推给肉眼。

先定义标识符,再实现验证

显示名称可以富有表现力,账户标识却承担稳定引用和权限关联,二者不必使用同一套规则。一个产品可以允许显示名称包含多语言文本,同时为不可变的内部身份使用独立编号。这样,用户修改名字或相似度规则更新时,不会直接改变数据归属。

如果公开标识符允许 Unicode,应明确允许哪些字符、怎样处理大小写、是否采用特定规范化,以及冲突发生时怎样解决。注册、登录、查找、后台管理必须使用一致规则。否则注册页认为两个名称不同,管理页却把它们合并显示,安全问题就来自系统内部的不一致,而不只是某个特殊字符。

策略变更尤其需要迁移计划。扩大允许范围可能带来新冲突,收紧范围可能使已有用户无法正常使用。可以先对现有数据离线评估,统计受影响的类型,再设计兼容期和用户通知。不要在一次库升级后无声地重算所有标识符,然后假定历史名称仍然保持原来的唯一性。

错误提示应引用当前字段的规则,而不是评价用户的语言。例如可以说明该公开编号暂不支持某类字符,并保留已经输入的内容;显示名称仍按产品规则正常保存。如果暂时限制较严,也应记录这是一项产品取舍。把实现能力的边界误称为文字本身不合法,会妨碍团队以后改进多语言支持。

规范化不会自动解决欺骗问题

规范化处理的是标准规定的等价关系,不是所有视觉相似性。某些不同表示可以在 NFC 下统一,某些兼容形式可在 NFKC 下折叠,但不同文字里形似的字母并不会因此全部变成同一个字符。把规范化当成万能去伪装工具,会同时高估保护能力和低估内容变化。

应把原始值与用于匹配的派生值分开考虑。文章正文可能保留原文,搜索索引另建匹配形式;标识符则遵循明确且版本可控的规则。涉及登录凭据时,不要套用通用文本清理按钮,更不要擅自给现有密码增加规范化或去空白步骤。任何凭据处理策略都必须由该认证系统明确规定并保持一致。

还要留意处理顺序。先验证后转换,与先转换后验证可能得到不同结果。如果转换能产生原本不允许的字符,验证流程就需要重新设计。与其堆叠一串看似安全的过滤器,不如为每个阶段写出输入、输出及不变量,然后用代表性样本验证完整路径是否满足最初的业务约束。

网址显示需要额外的上下文

国际化域名让非拉丁文字进入域名系统,其用户可见形式与协议使用的表示涉及专门规则。一个带非 ASCII 字符的域名不因此可疑,一个看起来全是熟悉字母的域名也不因此可信。评估链接时,应理解实际主机名和来源语境,避免仅凭短暂看到的外观作判断。

如果产品需要展示外部链接,可以让主机信息足够清楚,在必要的检查界面提供实际表示,并把跳转行为与用户预期对应起来。不要把任意一段像网址的显示文本当成目的地证明,也不要承诺某种字体能消除所有形似风险。普通的 ASCII 字符之间同样可能产生混淆。

对内容编辑而言,引用来源时使用清楚的标题和可核对的链接,比在文章里复制一串相似字符更有帮助。需要演示字符差异时,用码位说明和无真实目标的示意文本即可。这样读者能学习判断方法,而不会误把安全文章里的展示样本当成真实服务入口。

清理前确认用途,清理后验证结果

一套实用处理流程可以从保留原文开始。接着在检查器里定位可疑字符,确认它在当前字段中是否有合法用途,再选择针对性的变换。操作结果应提供差异预览和撤销方式。若涉及账户或源代码,还要把处理结果送回对应的验证流程,不能因为页面上看起来正常就结束。

建立测试样本时,不只放入可疑文本,也放入应该被接受的多语言名称、组合附加符号、混合方向短句和完整 emoji 序列。误报同样是缺陷:系统若不断拒绝真实语言用户,他们可能被迫使用不准确的名字或难以交流。安全与语言支持的共同目标,是让身份与内容表达得清楚,而不是让字符集越来越贫乏。

清理完成以后,可以把原文和结果并排展示,并提供新增、删除和替换的数量。对不理解码位的用户,解释具体影响更有帮助,例如某个序列可能拆开,或某段方向可能改变。对于批量文件,则先用小样本试运行并保存映射记录,再处理剩余内容。清理是可验证的数据变换,应像其他修改一样能够追踪。

对于日常复制粘贴,最有用的习惯是遇到异常先检查,而不是反复重打或一键删除。对于开发者,最有用的设计是区分展示文本、稳定身份与匹配规则。不可见字符和形似字符不会凭空消失,但一旦它们能被观察、解释并按用途处理,问题就从令人困惑的现象变成可维护的工程行为。

参考资料

  1. Unicode Technical Standard #39: Unicode Security Mechanisms ↗
  2. Unicode Standard Annex #9: Unicode Bidirectional Algorithm ↗
  3. Unicode Standard Annex #31: Unicode Identifiers and Syntax ↗
  4. Unicode Technical Standard #55: Unicode Source Code Handling ↗
  5. W3C: Unicode Controls vs. Markup for Bidi Support ↗
  6. W3C: How to Use Unicode Controls for Bidi Text ↗
  7. Unicode Standard Annex #15: Unicode Normalization Forms ↗
  8. RFC 5890: Internationalized Domain Names for Applications ↗
  9. Unicode Technical Standard #51: Unicode Emoji ↗