mramydnei · 2014/06/09 14:58
from:https://speakerdeck.com/mathiasbynens/hacking-with-unicode
0x00 Unicode简介
很多人经常会把Unicode和utf-8的概念混淆在一起,甚至会拿它们去做比较。实际上这种比较是非常的荒谬的。这就犹如拿“苹果”和“僵尸”去做比较,当然我觉得可以是更夸张的例子。所以,首先需要声明的是Unicode不是字符编码。我们可以把Unicode看做是一个数据库,不同的码点(code point)会指向不同的符号(如下图所示)。这是一种很简单想法,当你说出一个符号的码点时,别人就会知道你在说的是什么符号。当然了,如果他不知道也可以去查询Unicode表。
下面以拉丁字母中大写的A为例:
拉丁字母大写A的码点就是: U+0041。又比如码点U+2603就会是指向一个雪人(snowman)的符号:
当然还有一些更有趣的符号,比如下面这位:
这个符号看上去是一坨屎在向你微笑,这符号虽然很搞笑,但这并不是重点。因为我们发现这次的c码点从4位变成了5位。那么码点的长度究竟应该是多少,又到底存在多少个码点呢?
U+000000 -> U+10FFFF
复制代码
上面这个范围就是码点的范围了,长度一目了然。至于真实存有符号的码点的数量就不是所有的码点的总和了。因为其中也包括一些预留位。 除此之外还需要我们了解的就是平面(plane)的概念。Unicode字符被分成17组编排,而其中的每一组我们都称其为平面(Plane)。 其中的第一组被称为基本多语言面(Basic Multilingual Plane)也可以简称为BMP。是我们平时最常用的平面。它的范围是: U+0000 -> U+FFFF 举个例子,如果你正在使用英语来来编写一篇文章,那么你所用的字符就可能全都来自这一个平面。其余的平面2-17的范围则是:
U+010000 -> U+10FFFF
复制代码
这16个平面占据了整个Unicode的75%,我相信来自这些平面的字符很容易就可以和基本多语平面做区分。因为只要码点的长度大于4,你就应该想到这个字符是来自2-17的平面。
0x01 编码
在我们对Unicode的基础知识有了一点了解之后,现在再让我们来谈谈编码。
上图中所展示的UTF-8,UTF-16,UTF-32,UTF-EBCDIC和GB18030都是一些完全支持Unicode的编码。我们发现这种编码数量并不多,而且它们也存在一些差异。拿UTF-32来讲,我们发现不论需要编码的字符是什么,这个编码都会使用4 bytes。从存储空间的角度来讲,这种设计是十分的不合理的。相对而言UTF-16会显得更合理一些,不过不难看出其中最合理的还是属UTF-8。比如,我们用utf-8编码的一个字符,如果是属于第一个区域(U+000000-U+00007F)那么它就只会占用1 bytes。 在探索编码的过程当中,你最多可能会被搜索引擎带到的可能是下面的页面:
如同你所看到的,这是IETF发布的RFC文档。很多人在去翻阅文档时,都会选择看RFC文档。原因很简单因为RFC是标准。在RFC文档中我们甚至可以查阅到Unicode是如何被设计的。但是作为黑客,我想这些应该不是你所感兴趣的。在这里我会更加推荐你去看下面的网页:
在Encoding Living Standard你可以阅读到一些更贴近真实世界的东西。不是因为对RFC有偏见,而是因为RFC中的标准和实际应用到浏览器中的情况往往都会存在一些差异。
0x02 JavaScript中的Unicode
在这个部分中,我们主要会谈论JavaScript中一些奇怪的现象,一些程序员会犯的错误以及如何去修复。就像很多其它语言一样,JavaScript中也会有很多奇怪的现象。比如有这样的网站来专门纰漏PHP中一些奇怪的现象。
同样的我们也有wtfjs。上面会纰漏一些JavaScript中的奇怪现象:
当然还有一些其它的问题。比如,比较著名的Punycode攻击,常用于注册看上去相似的域名进行钓鱼攻击(如下图所示)。对于Punycode攻击也有一些非常有意思的文档。比如,Mutillidae的作者Adrian在不久前发表的” Fun and games with Unicode”。
我们都知道在Javascript当中我们可以使用base16来表示一个字符,比如: 用 ‘x41’ 来表示大写拉丁字母”A” 我们也可以用Unicode来表示一个字符,比如: 用’u0041’也可以表示大写拉丁字母”A” 那我们之前提到的U+1F4A9(那个在向你微笑的一坨屎)又是怎么样的呢?这个问题说简单也简单,说复杂也复杂。在ECMAScript 6(下面将简称为ES)中我们可以这样来解决这个问题:
当然,我们也可以使用代理编码对(surrogate pairs):
下面再附上代理编码对的编码。当然了,你不需要真的去记住它。但如果你是一个经常会和Unicode打交道的人,那么你至少应该去去了解一下。
现在我们再聊一下字符串长度的问题。这也是一些开发者会常犯的错误。在这里需要我们记住的是,字符串长度并不等于字符个数。下面这张图应该很容基就能帮助我们理解到这个问题。
不要认为没有人会犯这样的错误。Twitter上就有这样的问题,我实际上应该被允许发140坨屎,但实际上我只能发70坨……。虽然这不是安全问题,但这仍然是个逻辑错误。
对于处理这个问题你可以使用我编写的punycode.js:
当然在ES6中也有别的解决方案:
但实际上问题往往没有上面描述的那么简单。因为javascript还对会一些字符进行escape:
这个问题甚至会比我们想象的还要更复杂一些:
所以ES6又为我们提供了Unicode标准化:
那么这个方案已经完美了么?其实也不然。因为我们往往会面临更复杂的挑战。比如下面的例子,我们看到的只有9个字符,然而结果会是116。修复这类问题我们还需要用到epic regex-fu.
同样的问题也存在于reverse函数中(看图中第三个reverse结果)
我们可以通过下图中提供的esrever来解决这个问题:
又比如String.fromCharCode()函数也只能处理U+0000~U+FFFF范围内的字符。在解决这个问题上时,我们依旧可以使用之前提到的代理编码对,Punycode.js又或者是ES6
除此之外还有charAt()和charCodeAt()函数只能取一半的unicode的问题(U+1F4A9只能取到U+D83D)也在ES6和ES7当中得到了解决。在ES7可以用at()来替代charAt(据说这个问题没赶上ES6的修正),在ES6中的可以使用codePointAt来代替charCodeAt()。存在类似问题的函数还有很多,比如:slice(),substring()等等等等。 然后就是一些正则匹配的问题。也分别在ES6当中得到了解决。(翻到这儿感觉像是推ES6的有没有……)
当然这些也许只是JS开发者在开发的过程当中会碰到的问题的一小部分。对于测试这类问题我推荐你使用“那坨屎”来进行验证^_^
0x03 Mysql中的Unicode
谈完了JavaScript中Unicode问题之后,让我们再来看看Mysql中Unicode的表现又是如何的。下面是个很常见的例子,网上也有很多Mysql教程是教你这么去建数据库的。
请注意这里的编码选择的是UTF-8。这也会有问题?请看下图:
从上图中我们可以看到当我们使用UTF-8编码来建立数据库,并试图更新id=9001的字段column_name为’fooU+1F4A9foo’时,虽然更新成功了,但出现了警告。当我们对当前表的该字段进行查询时,发现内容居然被截断了。因为这里的UTF-8并不是我们所认识的支持所有unicode的UTF-8。所以作为开发者我们需要记住,应该使用utf8mb4来对数据库的编码进行设定,而不是使用utf-8.这种截断在某些场景下将导致很严肃的安全问题。
0x04 Hacking with Unicode
在做了这么多的铺垫之后,让我们来看一下真实生活当中存在的Hacking.
<1> UTF-16/UTF-32可能导致XSS问题(谷歌实例)
<2>Unicode将导致用户名欺骗问题
<3>Unicode换行符将导致问题XSS
<4>Mysql unicode截断问题,导致注册邮箱限制绕过
<5>Mysql unicode截断问题,导致WP插件被爆菊:
<6>Unicode问题导致Stackoverflow的HTML净化器被绕过:
0x05 写在最后
如果你是一个JavaScript开发者,我觉得这是一个你不容错过的文章。文章中不但给出了一些在开发过程中很有可能碰到的问题,也给出了相应的解决方案。如果你是黑客,文章中给出的检验这种缺陷的方法也许可以给你带来不少的收获。至少Mysql的截断问题,在我看来还是十分有趣的。