天天看點

前端開發中的字元編碼詳解

前端開發過程中會接觸各種各樣的編碼,比較常見的主要是utf-8和html實體編碼,但是web前端的世界卻不止這兩種編碼,而且編碼的選擇也會

造成一定的問題,如前後端開發過程中不同編碼的相容、多位元組編碼可能會造成的xss漏洞等。是以,本文旨在更好的全面了解涉及前端開發領域的字元編碼,避

免可能出現的互動和開發中的忽視的漏洞。

前端開發中的字元編碼詳解

url編碼

escape/unescape函數針對寬字元做unicode編碼,并針對碼值做十六進制編碼,是以使用escape針對漢字編碼會得到形

如”\uxxxx”的結果;encodeuri/decodeuri,encodeuricomponent/decodeuricomponent函數

針對寬位元組編碼卻不同于escape,首先針對寬位元組字元進行utf-8編碼,然後針對編碼後的結果進行“%”替換,得到結果。以上所述都是針對寬位元組字

符而言,對于編碼靠前的ascii字元而言,上述三組函數的安全字元的範圍也有所不同,具體可在上文中了解。

base64編碼

base64編碼在前端通常用于圖檔和icon的編碼,它将每3個8位位元組為一組,分成4組6位位元組,并且每個位元組的高位補零,形成4個8位的字

節,由此可看出base64編碼是可逆推的。在大多數浏覽器中,提供了ascii字元的base64編碼函數,即window.btoa()。該函數無法

針對寬位元組進行base64編碼,若針對中文編碼,則需現轉換位utf-8編碼,然後進行base64編碼。

function unicodetobase64(s){ return window.btoa(unescape(encodeuricomponent(s))) } 

通過encodeuricomponent對寬位元組字元編碼,是“%xx”形式的編碼,與utf8編碼的差別僅在于字首(這是由規範rfc3986決定的,将非asc字元進行某種形式編碼,并轉換為16進制,并在位元組前加上“%”)。是以通過unescape(encodeuricomponent(s))可以轉化為utf8位元組。當然,也可自己寫一個轉換函數,按照一定規則便行為utf-8編碼的位元組,如下例:

``` 

unescape(encodeuricomponent("中國")) //結果:"中国" 

encodeuricomponent("中國") //結果:"%e4%b8%ad%e5%9b%bd" 

console.log("\u00e4\u00b8\u00ad\u00e5\u009b\u00bd") // 結果: "中国" 

通過簡單的replace函數,就可以完成url編碼到utf8編碼的轉換,進而完成寬位元組字元到base64編碼的轉換。有了這個函數,我們手動生成一些data uri形式的内容,隻需制定mime類型和編碼方式,就可以實作文本的轉換,如以下代碼:

```

<a href="data:text/html;charset=utf-8;base64,phnjcmlwdd5hbgvydcgxmik8l3njcmlwdd4=" >abc</a> 

// 未編碼前:<a href="javascript: alert(1)">test</a> 

前端utf8編碼與後端gbk編碼的相容

目前前端大都采用utf8進行編碼,不管是html、js抑或是css,而後端則由于曆史原因大都采用gbk或gb2312進行解碼,是以前端通過

parameter傳遞的url編碼的字元串就不可能直接在背景進行解碼,為了更好的相容性,前端可進行兩次url編碼,即

encodeuricomponent(encodeuricomponent(“中國”)),這樣後端接收到參數後,先使用gbk或gb2312解碼,

得到了utf8編碼後再使用utf8解碼即可。兩次編碼主要是利用“asc字元使用gbk或gb2312編碼不變”的特點完成,富有技巧。

html實體編碼與進制編碼

實體編碼針對html的預留字元而言,如“<>”等。實體編碼有兩種形式&實體名;或&entity_number;,由于浏覽器對&實體名;的相容性有差别,是以最好采用實體号的形式編碼。

進制編碼,顧名思義将asc字元對應的碼值按照十六進制或十進制編碼,并轉化為&#x;(16進制)或&#d;(10進制)形式。

單單針對實體編碼而言并沒有什麼特殊強調的點,之是以把它單獨列為一個章節,意在強調這兩種編碼與js代碼的作用域的關系。

<div onclick="document.write('<img src=1 onerror=alert(23)>')">cccc</div> 

<div onclick="document.write('<img src=1 onerror=alert(23)>')">cccc</div> 

<img src=1 onerror=alert(23)> 

<script> 

   document.write('<img src=1 onerror=alert(23)>'); 

   document.write('<img src=1 onerror=alert(3)>'); 

   document.write('<img src=1 onerror=alert(23)>') 

   document.write('\u003c\u0069\u006d\u0067\u0020\u0073\u0072\u0063\u003d 

\u0031\u0020\u006f\u006e\u0065\u0072\u0072\u006f\u0072\u003d\u0061 

\u006c\u0065\u0072\u0074\u0028\u0032\u0033\u0029\u003e') 

</script> 

代碼中列舉了8個例子,第一個在事件處理函數onclick中輸出html片段;第二個則輸出經實體編碼後的html片段;第三個則是直接針對<img src=1 onerror=alert(23)>做16進制編碼;第四個則是針對onerror事件處理函數做16進制編碼;第五個則是在腳本中輸出實體編碼的字元;第六個針對事件處理函數做16進制編碼;第七個則針對所有的字元做16進制編碼;第八個則是在script中直接輸出<img src=1 onerror=alert(23)>的unicode編碼。

對比結果,前兩個例子在點選後都會彈出alert;第三個例子則在頁面中顯示文本<img src=1 onerror=alert(23)>;

第四個例子則會在頁面加載初期彈出alert;第五、七會輸出字元串;第六、八則會在第四個例子中的alert之後也彈出alert。現在分析這些結果,

通過第一二個例子可知道,html标簽中(除script标簽)的内聯js代碼可以進行html實體編碼,這是非常重要的一點,我們可以更為明确的進行驗

證:

<div onclick="alert('<img src=1 onerror=alert(23)>')">cccc</div> 

輸出的結果自然是<img src=1 onerror=alert(23)>,這的确論證了我們上文提到的這一點;第三個例子說明了html解析器在進行詞法分析前,首先進行解碼,十六進制和十進制皆可,是以,結果自然輸出形如<img src=1 onerror=alert(23)>的

字元串;第四個例子則緊接着論證了内聯在html的并采用十六進制編碼的js代碼同樣會被正确解析并執行,這說明了進制編碼同樣可被html解析器解析;

第五、七個例子說明在js中同樣可以使用實體編碼和進制編碼,解析的結果會渲染在頁面上;第六個例子則論證了上一觀點,隻針對事件處理函數做進制編碼,執

行後頁面彈出alert;第八個例子則是在js中執行unicode編碼的字元串,正常alert。

由此可見,js代碼内聯在html的非script标簽内,則會遵守html編碼規範:進制編碼和實體編碼;而在js代碼(script标簽内以及js檔案内)中,則遵從js編碼:1,unicode形式編碼(\uxxxx)2,普通的16進制編碼(\xh),這可通過第八個例子得到證明。之是以在本節提到這麼多編碼特點,主要提醒大家在預防xss時需要注意的幾點:

檢測使用者輸入時,不僅僅需要防範類似“<>”這樣的字元,通過unicode編碼或進制編碼仍有可能注入代碼

需要針對特定的關鍵字做過濾,如“eval、write、prototype”

盡可能禁止内聯事件處理函數的使用

js過濾“src/href/action”屬性,如“javascript:”,”data:”

js編碼

其實在上節中已提到了js編碼,即js可執行unicode編碼和十六(八)進制編碼後的字元串,但是不支援十進制編碼的字串。具體操作可通過常用

的幾個函數來實作,如“eval,write,settimeout,function”執行編碼後的字元串;同樣,對于十進制編碼的字串,通過結合

string.fromcharcode和eval同樣可以執行。

在此附上筆者實作的字元轉換,更為靈活的實作各種自定義形式的字串編碼:

var code = {}; 

/** 

* @param str 待編碼字串 

* @param jinzhi 進制編碼 

* @param prefix 字首 

* @param postfix 字尾 

* @param count 總共編碼的位數,預設為4 

* @returns {string} 

*/ 

code.encode = function({str = '',jinzhi = '16',prefix = '\\u',postfix = ';',count = '4'} = {}){ 

    var ret = ''; 

    var addzero,tmp; 

    for(let i=0;i<str.length;i++){ 

tmp = str.charcodeat(i).tostring(jinzhi); 

addzero = count - tmp.length + 1; 

ret += prefix + new array(addzero).join('0') + tmp + postfix; 

    } 

    return ret; 

}; 

code.decode = function({str = '',jinzhi = '16',prefix = '\\u',postfix = ';'} = {}){ 

    var splits = str.split(';'); 

    for(let i=0;i<splits.length;i++){ 

let tmp = splits[i].replace(prefix,''); 

ret += string.fromcharcode(parseint(tmp,jinzhi)); 

console.log(code.encode({str: '<img src=@ onerror=alert(123) />'})); 

console.log(code.decode({str: code.encode({str: '<img src=@ onerror=alert(123) />'})})) 

另外,對于js輸出點的過濾其實并不僅限于上文提到的如eval、settimeout、function等幾個,由于js文法比較靈活相對“漏洞”較多,可使用的“線索”也越豐富,如前段時間在stackoverflow上發現的一個問題,即

(0)['constructor']['constructor']('return "abc;"')() 

同樣可以執行js代碼,确實挺有特點的,具體為什麼上述形式可以執行代碼,請讀者自己仔細品味。

來源:51cto

繼續閱讀