作者:
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+00000w-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的截斷問題,在我看來還是十分有趣的。
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!