资讯专栏INFORMATION COLUMN

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

only_do / 2070人阅读

摘要:前言提供了函数,用于转义字符串,替换和字符为字符实体。如果希望正确地显示预留字符,我们必须在源代码中使用字符实体。字符实体有两种形式。转义我们的应对方式就是将取得的值中的特殊字符转为字符实体。

前言

underscore 提供了 _.escape 函数,用于转义 HTML 字符串,替换 &, <, >, ", ", 和 ` 字符为字符实体。

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

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

_.unescape("Curly, Larry & 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=

就相当于:

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

会有什么效果呢?

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

这是因为:

根据 W3C 规范,script 标签中所指的脚本仅在浏览器第一次加载页面时对其进行解析并执行其中的脚本代码,所以通过 innerHTML 方法动态插入到页面中的 script 标签中的脚本代码在所有浏览器中默认情况下均不能被执行。

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

你把地址改成 www.example.com/user.html?name= 呢?

就相当于:

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

整理下其中 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,显示的效果都是 <

<
<
<

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

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

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

转义

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

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

var name = getQueryString("name"); // 123

如果我们直接:

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

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

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

思考

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

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

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

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

服务器端渲染的代码为:

function render (input) {
  return ""
}

input 的值如果直接来自于用户的输入,用户可以输入 "> ,最终渲染的 HTML 代码就变成了:

 ">

结果又是一次 XSS 攻击……

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

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

& --> &
< --> <
> --> >
" --> "
" --> '
` --> <

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

_.escape

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

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 系列

underscore 系列目录地址:https://github.com/mqyqingfeng/Blog。

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

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93813.html

相关文章

  • Underscore源码中文注释(转)

    摘要:创建一个全局对象在浏览器中表示为对象在中表示对象保存下划线变量被覆盖之前的值如果出现命名冲突或考虑到规范可通过方法恢复被占用之前的值并返回对象以便重新命名创建一个空的对象常量便于内部共享使用将内置对象的原型链缓存在局部变量方便快速调用将 // Underscore.js 1.3.3 // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc....

    Guakin_Huang 评论0 收藏0
  • underscore 的源码该如何阅读?

    摘要:所以它与其他系列的文章并不冲突,完全可以在阅读完这个系列后,再跟着其他系列的文章接着学习。如何阅读我在写系列的时候,被问的最多的问题就是该怎么阅读源码我想简单聊一下自己的思路。感谢大家的阅读和支持,我是冴羽,下个系列再见啦 前言 别名:《underscore 系列 8 篇正式完结!》 介绍 underscore 系列是我写的第三个系列,前两个系列分别是 JavaScript 深入系列、...

    weknow619 评论0 收藏0
  • underscore 系列实现一个模板引擎(下)

    摘要:前言本篇接着上篇系列之实现一个模板引擎上。字符串中的每个字符均可由一个转义序列表示。在中,有四个字符被认为是行终结符,其他的折行字符都会被视为空白。 前言 本篇接着上篇 underscore 系列之实现一个模板引擎(上)。 鉴于本篇涉及的知识点太多,我们先来介绍下会用到的知识点。 反斜杠的作用 var txt = We are the so-called Vikings from th...

    gyl_coder 评论0 收藏0
  • Underscore.js 1.8.3 学习笔记

    摘要:值得注意的是,如果值在前面也就是值小于值,那么值域会被认为是零长度,而不是负增长。 underscore.js源码加注释一共1500多行,它提供了一整套函数式编程实用的功能,一共一百多个函数,几乎每一个函数都可以作为参考典范。初读的时候,真是一脸懵圈,各种函数闭包、迭代和嵌套的使用,让我一时很难消化。在这里,我来记录一下我学习underscore.js的一些发现,以及几个我认为比较经典...

    springDevBird 评论0 收藏0
  • underscore 系列实现一个模板引擎(上)

    摘要:第一版我们来尝试实现第一版第一版为了验证是否有用文件文件完整的可以查看示例一在这里我们使用了,实际上在文章中使用的是构造函数。构造函数创建一个新的对象。 前言 underscore 提供了模板引擎的功能,举个例子: var tpl = hello: ; var compiled = _.template(tpl); compiled({name: Kevin}); // hello:...

    LeexMuller 评论0 收藏0

发表评论

0条评论

only_do

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<