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的编码与解码总结
posted @ 2016-01-18 09:42  默语  阅读(3602)  评论(4编辑  收藏  举报