零宽度字符:和谐?屏蔽?不存在的

15,407 阅读6分钟

对零宽度字符完全没有头绪的可以先玩下这个Demo

什么是零宽度字符?

零宽度字符是一些不可见的,不可打印的字符。它们存在于页面中主要用于调整字符的显示格式,下面就是一些常见的零宽度字符及它们的unicode码和原本用途:

  1. 零宽度空格符 (zero-width space) U+200B : 用于较长单词的换行分隔
  2. 零宽度非断空格符 (zero width no-break space) U+FEFF : 用于阻止特定位置的换行分隔
  3. 零宽度连字符 (zero-width joiner) U+200D : 用于阿拉伯文与印度语系等文字中,使不会发生连字的字符间产生连字效果
  4. 零宽度断字符 (zero-width non-joiner) U+200C : 用于阿拉伯文,德文,印度语系等文字中,阻止会发生连字的字符间的连字效果
  5. 左至右符 (left-to-right mark) U+200E : 用于在混合文字方向的多种语言文本中(例:混合左至右书写的英语与右至左书写的希伯来语),规定排版文字书写方向为左至右
  6. 右至左符 (right-to-left mark) U+200F : 用于在混合文字方向的多种语言文本中,规定排版文字书写方向为右至左

零宽度字符能做什么?

1. 传递隐密信息

利用零宽度字符不可见的特性,我们可以用零宽度字符在任何未对零宽度字符做过滤的网页内插入不可见的隐形文本。下面是一个简单的利用零宽度字符对文本进行加密解密JavaScript例子:

加密
// 为了代码的简洁与易读性,以下代码会忽略性能方面考量

const text = '123😀';

// Array.from 能让我们正确读取宽度为2的Unicode字符,例:😀
const textArray = Array.from(text);

// 用codePointAt读取所有字符的十进制Unicode码
// 用toString将十进制Unicode码转化成二进制(除了二进制,我们也可以使用更大的进制来缩短加密后的信息长度,以此提升效率)
const binarify = textArray.map(c => c.codePointAt(0).toString(2));

// 此时binarify中的值是 ["110001", "110010", "110011", "11111011000000000"],下一步我们需要将"1","0"和分隔符映射到响应的零宽度字符上去

// 我们用零宽度连字符来代表1,零宽度断字符来代表0,零宽度空格符来代表分隔符
// 下面的''看上去像是空字符串,但其实都是长度为1,包含零宽度字符的字符串
const encoded = binarify.map(c => Array.from(c).map(b => b === '1' ? '‍' : '‌').join('')).join('​');

// 此时encoded中包含的就是一串不可见的加密文本了

注:在使用零宽度字符进行加密时,请尽量避免将加密后的隐形文本插入在明文的开头或者结尾处,以此来避免隐形文本在复制时被遗漏

解密
// 接着上面的encoded
// 用分隔符(零宽度空格符)提取加密文本中的字符
const split = encoded.split('​');

// 将文本转回成二进制数组
const binary = split.map(c => Array.from(c).map(z => z === '‍' ? '1' : '0').join(''));

// 此时binary中的值再次回到开始的 ["110001", "110010", "110011", "11111011000000000"]

// 最后一部只需要将二进制文本转回十进制,再使用 String.fromCodePoint 就可以得到原文本了
const decoded = binary.map(b => String.fromCodePoint(parseInt(b, 2))).join('');

// 此时decoded中的值即是 "123😀"

应用
  1. 隐形水印

    通过零宽度字符我们可以对内部文件添加隐形水印。在浏览者登录页面对内部文件进行浏览时,我们可以在文件的各处插入使用零宽度字符加密的浏览者信息,如果浏览者又恰好使用复制粘贴的方式在公共媒体上匿名分享了这个文件,我们就能通过嵌入在文件中的隐形水印轻松找到分享者了。

  2. 加密信息分享

    通过零宽度字符我们可以在任何网站上分享任何信息。敏感信息的审核与过滤在当今的互联网社区中扮演着至关重要的角色,但是零宽度字符却能如入无人之境一般轻松地穿透这两层信息分享的屏障。对比明文哈希表加密信息的方式,零宽度字符加密在网上的隐蔽性可以说是达到了一个新的高度。仅仅需要一个简单的识别/解密零宽度字符的浏览器插件,任何网站都可以成为信息分享的游乐场。

2. 逃脱词匹配

// 利用零宽度字符来分隔敏感词
const censored = '敏感词';

let censor = censored.replace(/敏感词/g, ''); // ''

// 使用零宽度空格符对字符串进行分隔
const uncensored  = Array.from(censored).join('​');

censor = uncensored.replace(/敏感词/g, ''); // '敏​感​词'

应用
  1. 逃脱敏感词过滤

    通过零宽度字符我们可以轻松逃脱敏感词过滤。敏感词自动过滤是维持互联网社区秩序的一项重要工具,只需倒入敏感词库和匹配相应敏感词,即可将大量的非法词汇拒之门外。使用谐音与拼音来逃脱敏感词过滤会让语言传递信息的效率降低,而使用零宽度字符可以在逃脱敏感词过滤的同时将词义原封不动地传达给接受者,大大提高信息传播者与接受者之间交流的效率。

示例与小结

为了更好地理解与使用零宽度字符,我为大家提供了一个Demo工具库,库中提供了一些应用零宽度字符的常见方法(加密解密逃脱匹配...)。零宽度字符在页面中的存在可能是一个好事,但也可能是一个坏事,一切都取决于你如何去使用零宽度字符。如果你不想在你的页面中看到这些零宽度字符,你可以选择完全过滤这些字符,但是这样会造成一些特殊语言的排版问题。所以,请酌情谨慎处理这些隐形的字符。

最后给各位留一个小彩蛋

“I‏‍‏‌‏‎‍​‏‏‌‏‌‏​‏‌‌‌‍‎‍​‏‏‏‎‎​‏‏‌‏‌‏​‌‎‏‏‍‍​‏‏‎‎‎‍​‌‎‌​‏‍​‏‍​‌‏‎​‌‏‎​‌‎​‏​‌‎‌t’s not who I am underneath, but what I do that defines me.” -Bruce Wayne

reference

Be careful what you copy: Invisibly inserting usernames into text with Zero-Width Characters by umpox