方向控制字符:看不见的字符如何重排显示顺序
一段文本在内存里的存储顺序(逻辑序,也是 ===、正则、编译器逐字节看到的顺序)和它在屏幕上的
显示顺序(视觉序)不一定相同。当 LTR(从左到右,如拉丁 / CJK)与 RTL(从右到左,如希伯来 / 阿拉伯)混排时,
浏览器按 Unicode Bidirectional Algorithm(UAX #9) 把文本重新排布——这是双向文字能正确显示的根基。
而一小撮看不见的 \p{Cf} 控制字符(mark / embedding / override / isolate)能人为操纵这个重排:
正经用途是修排版,恶意用途就是 Trojan Source——让源码看到的和编译的不一样。
本页系统拆开这套机制:特殊用意的码点、空白与隐形字符、
受控输入几页都把 U+202E 当「隐形字符之一」顺带点过,这里把整套算法和字符家族讲透。
逻辑序 vs 视觉序:同一串文本的两种顺序
输入一段混排文本(或点预设 / 注入下方控制字符)。渲染区把原串原样喂给浏览器的 bidi 引擎;下面两行格子是
实测结果:「逻辑序」按 code point 的存储顺序(0→N)逐格排开,「视觉序」用 getBoundingClientRect
读出每格在屏幕上的真实横坐标、按从左到右重排——格子里标的 #N 是它的逻辑来源下标。
两行对不上的格子(高亮)就是被 bidi 算法调换了位置的字符。
0 → N
左 → 右
强类
L(强左,拉丁 / CJK)、R(强右,希伯来)、AL(强右,阿拉伯)决定方向;
弱类 EN/AN(欧洲 / 阿拉伯数字)方向跟随上下文;中性 ON/WS(标点 / 空格)被两侧夹着走;
cf 是显式控制字符。(分类为教学近似,完整表见 UAX #44 DerivedBidiClass。)
RLO(U+202E)更暴力——它强制把后续字符(连普通 ASCII 也算)当强右处理并反转,这正是 Trojan Source 的杠杆。
方向控制字符全家:三代机制
操纵 bidi 的字符都是 \p{Cf}(看不见、不占宽),历史上分三代演进。点 注入 把它加到上面的输入框,看它如何改变重排:
RLE/LRE)和 override 会让内部文本
影响到范围之外的中性字符方向(俗称「泄漏」),嵌套时极易算错。isolate(LRI/RLI/FSI,Unicode 6.3 新增)
把括起来的整段当成一个中性对象对外隔离,内外互不干扰——拼接用户名 / 文件名等不可信片段时应当用它。
FSI(First Strong Isolate)更省心:自动按片段里第一个强方向字符定方向。三代都靠 PDF/PDI 收尾。
Trojan Source:让源码「看的和编译的不一样」
把方向控制字符塞进源代码的注释 / 字符串里,就能让人在 code review 里读到的逻辑、与编译器逐字节解析的逻辑南辕北辙 (Trojan Source · CVE-2021-42574)。下面切换两个攻击样本,三视图对照:
dir=auto,控制符已生效)[--],命中即拒。
现在 rustc、gcc、clang 会对源码里的 bidi 控制符告警,GitHub / GitLab 的 diff 也会高亮提示。
渲染不可信文本时用 isolate 包裹而非放任,或干脆把这些 \p{Cf} 过滤 / 可视化(如本页扫描器)。
White_Space=No 的隐形 \p{Cf}——在
空白与隐形字符页的全量扫描器里可按 bidi 方向控制分组查到;
U+202E 的「文件名伪装」现场见 特殊用意的码点;
把它注入受控输入框、并被计数器揭穿见 受控文本输入。
🔗 学一学 · 玩一玩
- UAX #9 · Unicode Bidirectional Algorithm(权威规范)· unicode.org 双向文字重排算法的源头标准:bidi class(L/R/AL/EN/AN/…)、embedding level、explicit formatting(LRE/RLE/LRO/RLO/PDF)与 isolate(LRI/RLI/FSI/PDI)的完整规则都定义于此。本页两行格子的「视觉序」就是浏览器实现这份算法的实测结果。
- Trojan Source · CVE-2021-42574· trojansource.codes Boucher & Anderson 的论文与示例仓库:用 bidi 控制符让源码的视觉表现与编译结果背离,影响几乎所有支持 Unicode 的语言。本页第三节的攻击样本即取自此。
-
CSS
unicode-bidi&<bdo>/<bdi>· developer.mozilla.org 在 HTML/CSS 里控制 bidi 的对应物:dir属性、unicode-bidi: isolate / bidi-override / plaintext、以及<bdi>(隔离不可信片段)、<bdo>(强制方向)——和本页讲的码点级控制字符一一对应。 - W3C i18n · Handling bidi text in markup· w3.org W3C 国际化组的实务指南:何时该用控制字符、何时该用标记(markup),以及拼接含 RTL 的动态内容时如何避免「泄漏」与错排。
- Codepoints · U+202E RIGHT-TO-LEFT OVERRIDE· codepoints.net 逐码点查证:本页用到的每个控制符(202A–202E / 2066–2069 / 200E·200F·061C)的 gc、Bidi_Class、所属 block 等属性一键可查。