WEB前端 [編碼] 規則淺析


前言

說到前端安全問題,首先想到的無疑是XSS(Cross Site Scripting,即跨站腳本),其主要發生在目標網站中目標用戶的瀏覽器層面上,當用戶瀏覽器渲染整個HTML文檔的過程中出現了不被預期的腳本指令並執行時,XSS就會發生。XSS有三類:

  • 反射型XSS:發出請求時,XSS代碼出現在URL中,作為輸入提交到服務端,服務端解析后響應,在響應內容中出現這段XSS代碼,最后瀏覽器解析執行,此過程就像一次反射;
  • 存儲型XSS:它與反射型XSS的差別僅在於--提交的XSS代碼會存儲在服務端,下次請求目標頁面時不用再提交XSS代碼。典型的例子就是留言板XSS,用戶提交一條包含XSS代碼的留言存儲到數據庫,再次查看留言時會顯示出來,進而觸發XSS攻擊。
  • DOM XSS:它與以上兩種XSS不同之處在於--DOM XSS不需要服務器解析響應的直接參與,觸發XSS靠的就是瀏覽器端的DOM解析,完全在客戶端發生。

XSS誘發原因有很多,很多網站做了各種針對性工作防御XSS,瀏覽器廠商也做了很大努力。為了防御XSS,很多可能觸發XSS的敏感字符會被過濾或轉義,而這些轉義規則也是各不相同的。不了解這些不同的編碼規則,會給我們日常編程造成很大的困惑,本文是針對各種編碼規則寫的一篇總結,希望給大家一些幫助。

1.字符編碼

字節:一字節由8位二進制數組成。

字符:肉眼看到的一個文字或者符號單元就是一個字符,一個字符可能對應1~n個字節。

字符集:一些字符組成的合集,如ASCII字符集就是由128個字符組成,基本上就是鍵盤上的英文字符(包括控制符)。

字符集編碼:一種字符集往往都對應於一種字符編碼方式。一個字符對應1~n字節是由字符集與編碼決定的,說白了字符集編碼就是一種字符與編碼值的映射關系。

常見的編碼方式有ASCII,GB2312,GBK,Big5,UTF-8,UTF-7等。不同的編碼方式,會產生不同的編碼結果,比如以GBK編碼的文件用UTF-8打開就會出現亂碼問題。如果文件是英文的,並不會出現亂碼。因為,在GBK中ASCII字符編碼是一個字節,繼承自ASCII碼,而漢字編碼是兩個字節;在UTF-8中ASCII字符依然是一個字節,和ASCII碼一樣,而漢字編碼是三或四個字節;所以,關於ASCII字符並不存在轉碼問題,其表示方式一致,而漢字需要重新轉碼。

其他編碼方式都是兼容ASCII的,ASCII字符編碼方式相同。

注:有些安全問題是由字符集使用不當造成的,所以在實際開發中需要選擇合適的編碼規則。

2.URL編碼

URL編碼是一種多功能技術,可以通過它來戰勝多種類型的輸入過濾器。URL編碼的最基本表示方式是使用問題字符的十六進制ASCII編碼來替換它們,並在ASCII編碼前加%。例如,單引號字符的ASCII碼為0x27,其URL編碼的表示方式為%27。

URL的一種常見的組成模式如下:

<scheme>://<netloc>/<path>?<query>#<fragment>

RFC3986文檔規定,Url中只允許包含英文字母(a-zA-Z)、數字(0-9)、-_.~4個特殊字符以及所有保留字符。

保留字符:Url可以划分成若干個組件,協議、主機、路徑等,RFC3986中指定了以下字符為保留字符:! * ' ( ) ; : @ & = + $ , / ? # [ ]。

不安全字符:還有一些字符,當他們直接放在Url中的時候,可能會引起解析程序的歧義。這些字符被視為不安全字符,原因有很多。

  • 空格:Url在傳輸的過程,或者用戶在排版的過程,或者文本處理程序在處理Url的過程,都有可能引入無關緊要的空格,或者將那些有意義的空格給去掉;
  • 引號以及<>:引號和尖括號通常用於在普通文本中起到分隔Url的作用;
  • #:通常用於表示書簽或者錨點;
  • %:百分號本身用作對不安全字符進行編碼時使用的特殊字符,因此本身需要編碼;
  • {}|\^[]`~:某一些網關或者傳輸代理會篡改這些字符。

需要注意的是,對於Url中的合法字符,編碼和不編碼是等價的,但是對於上面提到的這些字符,如果不經過編碼,那么它們有可能會造成Url語義的不同。因此對於Url而言,只有普通英文字符和數字,特殊字符$-_.+!*'()還有保留字符,才能出現在未經編碼的Url之中。其他字符均需要經過編碼之后才能出現在Url中。

如何進行URL編碼?

Url編碼通常也被稱為百分號編碼(Url Encoding,also known as percent-encoding),是因為它的編碼方式非常簡單,使用%百分號加上兩位的字符——0123456789ABCDEF——代表一個字節的十六進制形式。Url編碼默認使用的字符集是US-ASCII。例如a在US-ASCII碼中對應的字節是0x61,那么Url編碼之后得到的就是%61,我們在地址欄上輸入http://g.cn/search?q=%61%62%63,實際上就等同於在google上搜索abc了。又如@符號在ASCII字符集中對應的字節為0x40,經過Url編碼之后得到的是%40。

對於非ASCII字符,需要使用ASCII字符集的超集進行編碼得到相應的字節,然后對每個字節執行百分號編碼。對於Unicode字符,RFC文檔建議使用utf-8對其進行編碼得到相應的字節,然后對每個字節執行百分號編碼。如"中文"使用UTF-8字符集得到的字節為0xE4 0xB8 0xAD 0xE6 0x96 0x87,經過Url編碼之后得到"%E4%B8%AD%E6%96%87"。

如果某個字節對應着ASCII字符集中的某個非保留字符,則此字節無需使用百分號表示。例如"Url編碼",使用UTF-8編碼得到的字節是0x55 0x72 0x6C 0xE7 0xBC 0x96 0xE7 0xA0 0x81,由於前三個字節對應着ASCII中的非保留字符"Url",因此這三個字節可以用非保留字符"Url"表示。最終的Url編碼可以簡化成"Url%E7%BC%96%E7%A0%81" ,當然,如果你用"%55%72%6C%E7%BC%96%E7%A0%81"也是可以的。

注:不同的瀏覽器及不同的瀏覽器版本可能采用不同的URLEncode編碼規則,其編碼的敏感字符可能不完全相同。

3.HTML編碼

HtmlEncode:是將html源文件中不容許出現的字符進行編碼,通常是編碼以下字符:"<"、">"、"&"、"""、"'"等;

HtmlDecode:跟HtmlEncode恰好相反,解碼出原來的字符。

為了防止XSS攻擊,有的瀏覽器本身就會對某些HTML標簽內的內容進行處理,這樣我們就可以利用某些瀏覽器對這些標簽包含內容的轉義完成HTML編解碼。並不是所有的瀏覽器都會為標簽內置這樣的功能,但絕大多數瀏覽器都會支持JS,那么使用JS就是完成HTML編解碼就有更好的適用性。

下面是一些需要編碼的字符對應關系舉例:

  • &--&amp;
  • <--&lt;
  • >--&gt;
  • 空格--&nbsp;
  • “--&quot;

(還有一些其他的特殊字符,其轉義對應關系,請參考:HTML轉義字符

具體實現代碼如下:

 1 var HtmlUtil = {
 2     /*1.用瀏覽器內部轉換器實現html轉碼*/
 3     htmlEncode: function(html) {
 4         //1.首先動態創建一個容器標簽元素,如DIV
 5         var temp = document.createElement("div");
 6         //2.然后將要轉換的字符串設置為這個元素的innerText(ie支持)或者textContent(火狐,google支持)
 7         (temp.textContent != undefined) ? (temp.textContent = html) : (temp.innerText = html);
 8         //3.最后返回這個元素的innerHTML,即得到經過HTML編碼轉換的字符串了
 9         var output = temp.innerHTML;
10         temp = null;
11         return output;
12     },
13     /*2.用瀏覽器內部轉換器實現html解碼*/
14     htmlDecode: function(text) {
15         //1.首先動態創建一個容器標簽元素,如DIV
16         var temp = document.createElement("div");
17         //2.然后將要轉換的字符串設置為這個元素的innerHTML(ie,火狐,google都支持)
18         temp.innerHTML = text;
19         //3.最后返回這個元素的innerText(ie支持)或者textContent(火狐,google支持),即得到經過HTML解碼的字符串了。
20         var output = temp.innerText || temp.textContent;
21         temp = null;
22         return output;
23     },
24     /*3.用正則表達式實現html轉碼*/
25     htmlEncodeByRegExp: function(str) {
26         var s = "";
27         if (str.length == 0) return "";
28         s = str.replace(/&/g, "&amp;");
29         s = s.replace(/</g, "&lt;");
30         s = s.replace(/>/g, "&gt;");
31         s = s.replace(/ /g, "&nbsp;");
32         s = s.replace(/\'/g, "&#39;");
33         s = s.replace(/\"/g, "&quot;");
34         return s;
35     },
36     /*4.用正則表達式實現html解碼*/
37     htmlDecodeByRegExp: function(str) {
38         var s = "";
39         if (str.length == 0) return "";
40         s = str.replace(/&amp;/g, "&");
41         s = s.replace(/&lt;/g, "<");
42         s = s.replace(/&gt;/g, ">");
43         s = s.replace(/&nbsp;/g, " ");
44         s = s.replace(/&#39;/g, "\'");
45         s = s.replace(/&quot;/g, "\"");
46         return s;
47     }
48 };

注:會自動對其包含的敏感字符進行編碼,具備HTMLEncode功能的標簽有

  • <title></title>;
  • <textarea></textarea>;
  • <xmp></xmp>;
  • <iframe></iframe>;
  • <noscript></noscript>;
  • <noframes></noframes>;
  • <plaintext></plaintext>等。

4.JavaScript編碼

上邊講述了HTML編解碼的知識,一個網站並不僅僅包含HTML,還會帶有JS代碼,JS也有一些敏感的字符需要進行處理,當HTML和JS混在一起時,它們會采用什么樣的規則進行編解碼呢?下面有四個實例,可以了解一下其運作機理。

樣例1

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8"/>
 5     <title>樣例1</title>
 6 </head>
 7 <body>
 8     <input type="button" id="XSS" value="XSS" onclick="document.write('<img src=@ onerror=alert(1234) />')"/>
 9 </body>
10 </html>

運行結果:彈出-1234。

樣例2

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8"/>
 5     <title>樣例2</title>
 6     <script type = "text/javascript" >
 7         function HtmlEncode(str) {
 8             var s = "";
 9             if (str.length == 0) return "";
10             s = str.replace(/&/g, "&amp;");
11             s = s.replace(/</g, "&lt;");
12             s = s.replace(/>/g, "&gt;");
13             s = s.replace(/ /g, "&nbsp;");
14             s = s.replace(/\'/g, "&#39;");
15             s = s.replace(/\"/g, "&quot;");
16             return s;
17         }
18     </script>
19 </head>
20 <body>
21     <input type="button" id="XSS" value="XSS" onclick="document.write(HtmlEncode('<img src=@ onerror=alert(1234) />'))" />
22 </body>
23 </html>

運行結果:頁面輸出字符串--<img src=@ onerror=alert(1234) />。(chorme下沒有>輸出,應該進行過濾了)

樣例3

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8"/>
 5     <title>樣例3</title>
 6 </head>
 7 <body>
 8     <input type="button" id="XSS" value="XSS" onclick="document.write('&lt;img src=@ onerror=alert(1234) /&gt;')" />
 9 </body>
10 </html>

運行結果:彈出-1234。

樣例4

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8"/>
 5     <title>樣例4</title>
 6 </head>
 7 <body>
 8     <input type="button" id="XSS" value="XSS"/>
 9     <script type = "text/javascript" >
10         var btn = document.getElementById('XSS');
11         btn.onclick = function() {
12             document.write('<img src=@ onerror=alert(1234) />');
13             // document.write('&lt;img src=@ onerror=alert(1234) /&gt;');
14         }
15     </script>
16 </body>
17 </html>

運行結果:彈出-1234。

執行注釋代碼:頁面輸出字符串--<img src=@ onerror=alert(1234) />。(chorme下沒有>輸出,應該進行過濾了)

結果分析

對比樣例1樣例2可以看出,當HTML代碼段不被編碼時,頁面寫入的是一個IMG標簽,點擊后會觸發彈出框;而被編碼后再寫入頁面時,展現的是標簽的字符串形式,並沒有被當成img DOM渲染。

那對比樣例2樣例3 的執行結果,從二者document.write寫入頁面的字符串('&lt;img src=@ onerror=alert(1234) /&gt;')來說是相同的,但為什么會有不同的執行結果呢?兩個實例唯一的區別就是樣例3的寫入代碼是完全的<input>標簽內部,而樣例2的寫入代碼先由<script>內的HtmlEncode編碼后再寫入。樣例3中onclick里的這段JavaScript代碼出現在HTML中,在瀏覽器載入后,瀏覽器會對其自動解碼,所以在JavaScript執行前所要寫入的字符串已經是‘<img src=@ onerror=alert(1234) />’,所以點擊后會有彈出框。所以,樣例1樣例3執行結果相同。

再看樣例4,直接執行和執行注釋部分二者有不同的結果,執行注釋部分代碼,里面的'&lt;img src=@ onerror=alert(1234) /&gt;'會在JS執行前自動解碼嗎?根據其不同的執行結果,很明顯是不會自動解碼的,當用戶輸入的字符上下文環境是JavaScript,不是HTML(可以認為<script>標簽里的內容和HTML環境毫無關系)時,這段內容需要遵循JavaScript規則。

為了防止XSS攻擊,對於需要在JavaScript處理的字符,JavaScript也會其進行編碼,有以下幾種形式:

  • Unicode形式:\uH(十六進制);
  • 普通十六進制:\xH。
  • 純轉義:\',\",\<,\>這樣在特殊字符前加上\進行轉義。

如果在樣例4中寫入的字符串按照JavaScript編碼規則轉義為--'\<img src\=@ onerror=alert\(1234\) \/\>',執行代碼結果依然是彈出“1234”,並不是輸出字符串,這是因為在JS代碼中的代碼會在執行之前進行自動解碼,自動去掉轉義。即使進行Unicode和十六進制編碼,在執行前仍然會自動解碼。

如何進行編碼?

在JavaScript中有三套編碼/解碼函數,分別為:

  • escape/unescape;
  • encodeURL/decodeURL;
  • encodeURLComponent/decodeURLComponent;

它們都是將不安全不合法的Url字符轉換為合法的Url字符表示,其中一個很大的區別就是它們編碼的敏感字符集不同,對於下面的字符不會進行編碼:

  • escape:*/@+-._0-9a-zA-Z (69個),對0-255以外的unicode值進行編碼輸出格式為:%u**** (已經被W3C廢棄);
  • encodeURL:!#$&'()*+,/:;=?@-._~0-9a-zA-Z (82個),使用UTF-8對非ASCII字符進行編碼,然后再進行百分號編碼;
  • encodeURLComponent:!'()*-._~0-9a-zA-Z (71個),使用UTF-8對非ASCII字符進行編碼,然后再進行百分號編碼。

為了更好的理解,寫了一個函數來實現escape功能,代碼如下:

 1 var escape = function(str) {
 2     var _a, _b;
 3     var _c = "";
 4     for (var i = 0; i < str.length; i++) {
 5         _a = str.charCodeAt(i);
 6         _b = _a < 255 ? "%" : "%u"; // u不可大寫
 7         _b = _a < 16 ? "%0" : _b;
 8         _c += _b + _a.toString(16).toUpperCase();
 9     }
10     return _c;
11 }

escape函數是從Javascript 1.0的時候就存在了,其他兩個函數是在Javascript 1.5才引入的。但是由於Javascript 1.5已經非常普及了,所以實際上使用encodeURI和encodeURIComponent並不會有什么兼容性問題。

5.Base64編碼

Base64編碼可用於在HTTP環境下傳遞較長的標識信息。例如,在Java Persistence系統Hibernate中,就采用了Base64來將一個較長的唯一標識符(一般為128-bit的UUID)編碼為一個字符串,用作HTTP表單和HTTP GET URL中的參數。在其他應用程序中,也常常需要把二進制數據編碼為適合放在URL(包括隱藏表單域)中的形式。此時,采用Base64編碼不僅比較簡短,同時也具有不可讀性,即所編碼的數據不會被人用肉眼所直接看到。

Base64編碼要求把3個8位字節(3*8=24)轉化為4個6位的字節(4*6=24),之后在6位的前面補兩個0,形成8位一個字節的形式。 如果剩下的字符不足3個字節,則用0填充,輸出字符使用'=',因此編碼后輸出的文本末尾可能會出現1或2個'='。

為了保證所輸出的編碼位可讀字符,Base64制定了一個編碼表,以便進行統一轉換。編碼表的大小為2^6=64,這也是Base64名稱的由來。

Base64編碼過程

以下是一個Base64編碼過程舉例:

  1. 初始字符:s 1 3;
  2. ascii表示:115 49 51;
  3. 2進制(8個一組,3組):01110011 00110001 00110011;
  4. 重新分組(6個一組,4組): 011100 110011 000100 110011;
  5. 由於計算機是按照byte存儲的,也就是8位8位的存數,6位不夠,兩個高位自動補0;
  6. 二進制轉換為: 00011100 00110011 00000100 00110011;
  7. 轉換為十六進制:28 51 4 51;
  8. 根據Base64編碼表可得: c z E z。

由上例可知,初始字符“s13”就被轉換為了“czEz”,使需要傳輸的字符變得不可讀,一定程度上增加了安全性。

從網上找了一段JavaScript實現Base64的代碼,如下所示:

var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var base64DecodeChars = new Array(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1);

function base64encode(str) {
    var returnVal, i, len;
    var c1, c2, c3;
    len = str.length;
    i = 0;
    returnVal = "";
    while (i < len) {
        c1 = str.charCodeAt(i++) & 0xff;
        if (i == len) {
            returnVal += base64EncodeChars.charAt(c1 >> 2);
            returnVal += base64EncodeChars.charAt((c1 & 0x3) << 4);
            returnVal += "==";
            break;
        }
        c2 = str.charCodeAt(i++);
        if (i == len) {
            returnVal += base64EncodeChars.charAt(c1 >> 2);
            returnVal += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
            returnVal += base64EncodeChars.charAt((c2 & 0xF) << 2);
            returnVal += "=";
            break;
        }
        c3 = str.charCodeAt(i++);
        returnVal += base64EncodeChars.charAt(c1 >> 2);
        returnVal += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
        returnVal += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
        returnVal += base64EncodeChars.charAt(c3 & 0x3F);
    }
    return returnVal;
}

function base64decode(str) {
    varc1, c2, c3, c4;
    vari, len, returnVal;
    len = str.length;
    i = 0;
    returnVal = "";
    while (i < len) {
        /*c1*/
        do {
            c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
        } while (i < len && c1 == -1);
        if (c1 == -1) {
            break;
        }
        /*c2*/
        do {
            c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
        } while (i < len && c2 == -1);
        if (c2 == -1) {
            break;
        }
        returnVal += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4));
        /*c3*/
        do {
            c3 = str.charCodeAt(i++) & 0xff;
            if (c3 == 61) {
                return returnVal;
            }
            c3 = base64DecodeChars[c3];
        } while (i < len && c3 == -1);
        if (c3 == -1) {
            break;
        }
        returnVal += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2));
        /*c4*/
        do {
            c4 = str.charCodeAt(i++) & 0xff;
            if (c4 == 61) {
                return returnVal;
            }
            c4 = base64DecodeChars[c4];
        } while (i < len && c4 == -1);
        if (c4 == -1) {
            break;
        }
        returnVal += String.fromCharCode(((c3 & 0x03) << 6) | c4);
    }
    return returnVal;
}
Base64編解碼

結束語

由編碼規則產生的安全漏洞有很多,作為開發者要詳細了解不同編碼規則,對潛在的安全問題有所防御。有很多黑客會根據不同瀏覽器編碼特性及采用的編碼規則,利用特定的編碼方式可繞過安全防御,實現對網站的攻擊。在《Web前端黑客技術揭秘》一書中有很多講述,感興趣的同學可以讀一下。

參考文獻:

  1. URL編碼與解碼
  2. 每個web開發者都應該知道的URL編碼知識
  3. JavaScript處理HTML的編碼與解碼總結


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM