from:https://speakerdeck.com/mathiasbynens/hacking-with-unicode


0x00 Unicode簡介


很多人經常會把Unicode和utf-8的概念混淆在一起,甚至會拿它們去做比較。實際上這種比較是非常的荒謬的。這就猶如拿“蘋果”和“僵尸”去做比較,當然我覺得可以是更夸張的例子。所以,首先需要聲明的是Unicode不是字符編碼。我們可以把Unicode看做是一個數據庫,不同的碼點(code point)會指向不同的符號(如下圖所示)。這是一種很簡單想法,當你說出一個符號的碼點時,別人就會知道你在說的是什么符號。當然了,如果他不知道也可以去查詢Unicode表。

enter image description here

下面以拉丁字母中大寫的A為例:

enter image description here

拉丁字母大寫A的碼點就是: U+0041。又比如碼點U+2603就會是指向一個雪人(snowman)的符號:

enter image description here

當然還有一些更有趣的符號,比如下面這位:

enter image description here

這個符號看上去是一坨屎在向你微笑,這符號雖然很搞笑,但這并不是重點。因為我們發現這次的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的基礎知識有了一點了解之后,現在再讓我們來談談編碼。

enter image description here

上圖中所展示的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。 在探索編碼的過程當中,你最多可能會被搜索引擎帶到的可能是下面的頁面:

enter image description here

如同你所看到的,這是IETF發布的RFC文檔。很多人在去翻閱文檔時,都會選擇看RFC文檔。原因很簡單因為RFC是標準。在RFC文檔中我們甚至可以查閱到Unicode是如何被設計的。但是作為黑客,我想這些應該不是你所感興趣的。在這里我會更加推薦你去看下面的網頁:

enter image description here

在Encoding Living Standard你可以閱讀到一些更貼近真實世界的東西。不是因為對RFC有偏見,而是因為RFC中的標準和實際應用到瀏覽器中的情況往往都會存在一些差異。

0x02 JavaScript中的Unicode


在這個部分中,我們主要會談論JavaScript中一些奇怪的現象,一些程序員會犯的錯誤以及如何去修復。就像很多其它語言一樣,JavaScript中也會有很多奇怪的現象。比如有這樣的網站來專門紕漏PHP中一些奇怪的現象。

enter image description here

同樣的我們也有wtfjs。上面會紕漏一些JavaScript中的奇怪現象:

enter image description here

當然還有一些其它的問題。比如,比較著名的Punycode攻擊,常用于注冊看上去相似的域名進行釣魚攻擊(如下圖所示)。對于Punycode攻擊也有一些非常有意思的文檔。比如,Mutillidae的作者Adrian在不久前發表的” Fun and games with Unicode”。

enter image description here

我們都知道在Javascript當中我們可以使用base16來表示一個字符,比如: 用 ‘x41’ 來表示大寫拉丁字母”A” 我們也可以用Unicode來表示一個字符,比如: 用’u0041’也可以表示大寫拉丁字母”A” 那我們之前提到的U+1F4A9(那個在向你微笑的一坨屎)又是怎么樣的呢?這個問題說簡單也簡單,說復雜也復雜。在ECMAScript 6(下面將簡稱為ES)中我們可以這樣來解決這個問題:

enter image description here

當然,我們也可以使用代理編碼對(surrogate pairs):

enter image description here

下面再附上代理編碼對的編碼。當然了,你不需要真的去記住它。但如果你是一個經常會和Unicode打交道的人,那么你至少應該去去了解一下。

enter image description here

現在我們再聊一下字符串長度的問題。這也是一些開發者會常犯的錯誤。在這里需要我們記住的是,字符串長度并不等于字符個數。下面這張圖應該很容基就能幫助我們理解到這個問題。

enter image description here

不要認為沒有人會犯這樣的錯誤。Twitter上就有這樣的問題,我實際上應該被允許發140坨屎,但實際上我只能發70坨……。雖然這不是安全問題,但這仍然是個邏輯錯誤。

enter image description here

對于處理這個問題你可以使用我編寫的punycode.js:

enter image description here

當然在ES6中也有別的解決方案:

enter image description here

但實際上問題往往沒有上面描述的那么簡單。因為javascript還對會一些字符進行escape:

enter image description here

這個問題甚至會比我們想象的還要更復雜一些:

enter image description here

所以ES6又為我們提供了Unicode標準化:

enter image description here

那么這個方案已經完美了么?其實也不然。因為我們往往會面臨更復雜的挑戰。比如下面的例子,我們看到的只有9個字符,然而結果會是116。修復這類問題我們還需要用到epic regex-fu.

enter image description here

同樣的問題也存在于reverse函數中(看圖中第三個reverse結果)

enter image description here

我們可以通過下圖中提供的esrever來解決這個問題:

enter image description here

又比如String.fromCharCode()函數也只能處理U+0000~U+FFFF范圍內的字符。在解決這個問題上時,我們依舊可以使用之前提到的代理編碼對,Punycode.js又或者是ES6

enter image description here

除此之外還有charAt()和charCodeAt()函數只能取一半的unicode的問題(U+1F4A9只能取到U+D83D)也在ES6和ES7當中得到了解決。在ES7可以用at()來替代charAt(據說這個問題沒趕上ES6的修正),在ES6中的可以使用codePointAt來代替charCodeAt()。存在類似問題的函數還有很多,比如:slice(),substring()等等等等。 然后就是一些正則匹配的問題。也分別在ES6當中得到了解決。(翻到這兒感覺像是推ES6的有沒有……)

enter image description here

當然這些也許只是JS開發者在開發的過程當中會碰到的問題的一小部分。對于測試這類問題我推薦你使用“那坨屎”來進行驗證^_^

0x03 Mysql中的Unicode


談完了JavaScript中Unicode問題之后,讓我們再來看看Mysql中Unicode的表現又是如何的。下面是個很常見的例子,網上也有很多Mysql教程是教你這么去建數據庫的。

enter image description here

請注意這里的編碼選擇的是UTF-8。這也會有問題?請看下圖:

enter image description here

從上圖中我們可以看到當我們使用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問題(谷歌實例)

enter image description here

<2>Unicode將導致用戶名欺騙問題

enter image description here

<3>Unicode換行符將導致問題XSS

enter image description here

<4>Mysql unicode截斷問題,導致注冊郵箱限制繞過

enter image description here

<5>Mysql unicode截斷問題,導致WP插件被爆菊:

enter image description here

<6>Unicode問題導致Stackoverflow的HTML凈化器被繞過:

enter image description here

0x05 寫在最后


如果你是一個JavaScript開發者,我覺得這是一個你不容錯過的文章。文章中不但給出了一些在開發過程中很有可能碰到的問題,也給出了相應的解決方案。如果你是黑客,文章中給出的檢驗這種缺陷的方法也許可以給你帶來不少的收獲。至少Mysql的截斷問題,在我看來還是十分有趣的。

您的支持將鼓勵我們繼續創作!

[微信] 掃描二維碼打賞

[支付寶] 掃描二維碼打賞