【超精简JS模版库/前端模板库】原理简析 和 XSS防范
使用jsp、php、asp或者后来的struts等等的朋友,不一定知道什么是模版,但一定很清楚这样的开发方式:
<div class="m-carousel">
<div class="m-carousel-wrap" id="bannerContainer">
</div>
</div>
<ul class="catelist onepx" onepxset="" style="border: 0px; position: relative;" id="navApplication">
<div class="onepxHelper" id="onepx1"></div>
<%for(var i=0,len=data.types.length;i<len;i++){%>
<%var _ = data.types[i];%>
<%if(_.online){%>
<li data-nav="<%=_.type%>">
<i data-nav="<%=_.type%>" class="ico i-cate <%=_.class%> <%if(_.active){%>active<%}%>"></i>
<span data-nav="<%=_.type%>"><%=_.name%></span>
</li>
<%}%>
<%}%>
</ul>
各种各样的<%%>标记,这是典型的模板语法,而这就是HTML模版。
在HTML5时代,我们更多使用前端资源静态部署,更多场景下需要使用前端模板库把后台返回的JSON数据填充到页面中。前端使用模版库,比手工拼接字符串要优雅很多。
当然如果后端使用nodejs,前端模版库或者叫js模版库一样能兼容使用。
这里拿一个非常简洁的模版库作为介绍,作者John Resig也就是鼎鼎大名的jQuery创始人。代码只有聊聊可数的十几行:
// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
// http://ejohn.org/blog/javascript-micro-templating/
(function(){
var cache = {}; this.tmpl = function tmpl(str, data){
// Figure out if we're getting a template, or if we need to
// load the template - and be sure to cache the result.
var fn = !/\W/.test(str) ?
cache[str] = cache[str] ||
tmpl(document.getElementById(str).innerHTML) : // Generate a reusable function that will serve as a template
// generator (and which will be cached).
new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" + // Introduce the data as local variables using with(){}
"with(obj){p.push('" + // Convert the template into pure JavaScript
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'")
+ "');}return p.join('');"); // Provide some basic currying to the user
return data ? fn( data ) : fn;
};
})();
关键是三部分:
- 使用new Function,让字符串变成函数;
- 使用正则表达式替换拼接,这是最核心部分,也是最优雅的部分;
- 把用户传入的数据data作为作用域(使用with),填充到各个坑。
首先看一个使用例子,从使用的例子慢慢解剖John这个艺术品。
console.log(tmpl("<span data='<% print(1,2,{}); %>'><%=name?name:1+1+1 %></span>", {name: 'kenko'})); //print后必须加入分号,用于隔开
具体的语法就不多解释了,跟underscore的模版库基本一致,大家可以参考一下:http://underscorejs.org/#template
Chrome运行,将得到:
<span data='12[object Object]'>kenko</span>
这里使用了2个特性,一个是<%= %>直接输出value或计算结果,第二个是使用了内置的print方法,可以理解为evaluation,执行一些js逻辑。
那么接下来,我们深入看看模版tmpl函数里边到底做了什么?
1、看看最终生成的Function
new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" +
"with(obj){p.push('" +
"<span data=\''); print(1,2,{}); p.push('\'>',name?name:1+1+1 ,'</span>"
+ "');}return p.join('');");
Function的语法,大家可以看看w3cschool的解释,足够详细了:http://www.w3school.com.cn/js/pro_js_functions_function_object.asp
Function接受若干个参数,最后一个参数就是函数体字符串,前边的都是参数名。
关键是红色部分,这部分就是那些非常“艺术”的正则匹配替换,最终得到的字符串。
2、逐步看看正则表达替换是如何运作的
console.log(
str.replace(/[\r\t\n]/g, " ")
.split("<%").join('\t')
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split(/\t/).join("');")
.split("%>").join("p.push('")
.split(/\r/).join("\\'")
);
为了满足我们的窥探欲,我们把模版库的源代码抠出来,逐行打印看看。
console.log(
str.replace(/[\r\t\n]/g, " ")
.split("<%").join('\t')
// .replace(/((^|%>)[^\t]*)'/g, "$1\r")
// .replace(/\t=(.*?)%>/g, "',$1,'")
// .split(/\t/).join("');")
// .split("%>").join("p.push('")
// .split(/\r/).join("\\'")
);
运行将得到:
<span data=' print(1,2,{}); %>'> =name?name:1+1+1 %></span>
可以发现前半部<%都变成了一个制表符\t;
再逐行看看后续的输出,可以发现:
console.log(
str.replace(/[\r\t\n]/g, " ")
.split("<%").join('\t')
.replace(/((^|%>)[^\t]*)'/g, "$1\r") //关键一笔,为了兼容单引号,把单引号换成\r。<span data= \t\r print(1,2,{}); %> \r > \t =name?name:1+1+1 %></span>
.replace(/\t=(.*?)%>/g, "',$1,'") //核心,$1对应的就是括号内的内容,这个是正则表达式的功能。<span data= \t\r print(1,2,{}); %> \r >',name?name:1+1+1 ,'</span>
.split(/\t/).join("');") //跟上边的关键一笔对应。<span data= \r '); print(1,2,{}); %> \r >',name?name:1+1+1 ,'</span>
.split("%>").join("p.push('") //<span data= \r '); print(1,2,{}); p.push(' \r >',name?name:1+1+1 ,'</span>
.split(/\r/).join("\\'") //<span data=\''); print(1,2,{}); p.push('\'>',name?name:1+1+1 ,'</span>
);
john巧妙的利用\r、\t分别代表了单引号( ' )、左标记( <% ),因为这两个符号在后续的字符串替换中会有干扰,尤其是单引号,这也是我为什么在例子中故意让span的data属性用单引号包裹的原因。
配合前后的两句固定语句,其实就是把整个模版,换成一段代码:
with(obj){
p.push('<span data=\'');
print(1,2,{});
p.push('\'>'',name?name:1+1+1 ,'</span>');
}
return p.join('');
大概可以理解为:
<% ====> ')
%> ====> p.push('
= ====> ,$1,
原理就是字符串拼接,很简单,但正则表达式这种艺术范,我只能说只可意会不可言传了,对john的膜拜之情油然而生。
================================没有意义的分割线======================================
话锋一转,虽然john这个艺术品绝对的牛逼,但这个模版库不是绝对的好用。在实际开发中,我们需要时刻谨记XSS防范,在传统的jquery修改innerHTML的做法中,很容易中XSS。
而模版库到了最后,一样需要通过innerHTML注入到dom中。
那么,要么我们在传递给模版库前,自己对数据做足够的XSS检查,尤其是来自用户或第三方的数据,如果没有做特殊字符转义,就很容易受到XSS攻击。
一般简单来说,我们可以对准备填充的数据做简单的处理,关键是&"'等字符:
var esc = function (s) {
return s.toString()
.replace(/&#(\d{1,3});/g, function (r, code) { //这里目的是防止重复执行esc,导致一些字符重复转义
return String.fromCharCode(code);
}).replace(/[&'"<>\/\\\-\x00-\x09\x0b-\x0c\x1f\x80-\xff]/g, function (r) {
return "&#" + r.charCodeAt(0) + ";"
}).replace(/javascript:/g, "");
};
那么,如果模版库统一做XSS转义,事情就肯定能变得更简单。
所以,我们尝试把esc函数加入到模版库中。
模版库把用户数据注入dom的地方有两个:
- print函数
- .replace(/\t=(.*?)%>/g, "',$1,'"),也就是<%=name %>这样的地方。
由于new Function把函数体字符串变成实际函数,所以在函数中无法像平时那样,访问当前上下文(闭包),只能访问Function构建时指定的参数或者全局变量/方法。
那么,我们可以把esc作为参数,传给Function,模版库最终改为:
var fn = !/\W/.test(str) ?
cache[str] = (cache[str] || tmpl(document.getElementById(str).innerHTML)) :
new Function("obj", "esc",
"var p=[],print=function(){for(var i=0;i<arguments.length;i++){p.push(esc(arguments[i]));}};" + "with(obj){p.push('" + str.replace(/[\r\t\n]/g, " ")
.split("<%").join('\t')
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',esc($1),'") //esc不能是外部的局部变量,无法形成闭包。所以要么在函数内定义,要么做成全局函数,又或者作为参数
.split(/\t/).join("');")
.split("%>").join("p.push('")
.split(/\r/).join("\\'")
+ "');}return p.join('');"); // Provide some basic currying to the user
return data ? fn(data, esc) : function(param){return fn(param, esc)}; //curry办法。先返回一个编译好的render函数,用户可以延迟渲染
来个攻击的例子看看效果:
var name = '<script>alert(1)</script>呵呵呵呵呵';
var age = '\'onclick="alert(1)'
document.write(template('<span data="<%=age %>"><%=name %></span>', {name: name, age: age}));
假设我们获取url参数name和age,然后直接填入到页面中。如果使用原版的模版库,我们马上能看到。。。alert。。。当然,黑客可以换成实际有意义的代码,例如获取你密码,发个微博,发个空间,甚至转走你的虚拟金币。

仔细一看,dom满满都是攻击的代码

不单是页面刚打开的script标签式攻击,还有span节点的onclick攻击,当点击span的时候,又会执行一段js。。。
接下来,我们见证一下神奇的时刻!!!换成加入了XSS自动转义的模版库。
两处的攻击都被过滤了,只剩下乖巧的纯文本。嘿嘿


最后,说点关于underscore的,underscore的模版库原理跟john这个精简版类似,也是正则+字符串替换。
不过,不同点是,underscore更完善一些,它提供了两种注入数据的方式:
- <%=name %>,这个跟john的一样,没有做任何过滤;
- <%-name %>,这个有做几个关键字符的转义,包括& " '
当然,我们也可以把第一种模式也做成自动转义,正如我现在项目就需要这么搞。。。大概就是1239行那些代码,以下红色部分就是我修改的内容。
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':_.escape(__t))+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p=''," +
"print=function(){for(var i=0;i<arguments.length;i++){__p += _.escape(arguments[i]);}};\n" +
source + "return __p;\n";
【超精简JS模版库/前端模板库】原理简析 和 XSS防范的更多相关文章
- Handlebars模板库浅析
Handlebars模板库简单介绍 Handlebars是JavaScript一个语义模板库,通过对view(模板)和data(ajax请求的数据,一般是json)的分离来快速构建Web模板.它采用& ...
- 葡萄城报表模板库再次更新!补充医院Dashboard及房地产销售行业报表
新增模板介绍 近日,葡萄城报表再次对报表模板库进行了更新,除了补充医院用于整体运营监控的5张 Dashboard 报表外,还增加了房地产销售场景中常见的12张报表. 5张 Dashboard 报表模板 ...
- C/C++程序基础 (十一)标准模板库
标准模板库 标准模板库在标准函数库的定位 迭代器(类似指针)保证算法(常用算法)和容器(数据结构)的结合. vector的实现 底层实现是动态数组,所以支持随机访问. 内部是动态数组,随着insert ...
- 前端与编译原理 用js去运行js代码 js2run
# 前端与编译原理 用js去运行js代码 js2run 前端与编译原理似乎相隔甚远,各种热门的框架都学不过来,那能顾及到这么多底层呢,前端开发者们似乎对编译原理的影响仅仅是"抽象语法树&qu ...
- 前端JS模版库kino.razor - 原理流程分析 - 改进版轮子RazorJs
1.前言 从后台获取数据,在前端JS里面拼接字符串,不累吗?敢不敢找一款前端使使... 现在这种模板库比较多了,我用过的jquery-template .JsRender .听说过的一堆,还有各种MV ...
- 使用前端后台管理模板库admin-lte(转)
使用前端后台管理模板库admin-lte 使用前端后台管理模板库admin-lte 安装 搭建环境 安装 安装admin-lte,可以通过以下几种办法安装,下图是GitHub中admin-lte的主页 ...
- Flatic – 超齐全的 Web 元素界面素材库免费下载
Flatic 是一个庞大的用户界面工具包,包含数以百计的网页元素,这将有助于你在 Photoshop 中轻松设计整个网站.成套的图标和动作都已包含在套件中.该素材包包括超过100个 PSD 元素.您可 ...
- 前端组件库 - 搭建web app常用的样式/组件等收集列表(移动优先)
0. 前端自动化(Workflow) 前端构建工具 Webpack - module bundler Yeoman - a set of tools for automating developmen ...
- Boba.js – 用于 Google 统计分析 JavaScript 库
Boba.js 是一个小的,易于扩展的 JavaScript 库,让谷歌分析(Google Analytics)的更灵活,更容易.它同时支持旧的 ga.js 库以及新的 analytics.js 库. ...
随机推荐
- Android之防止反编译技巧
1. 判断程序是否运行在模拟器上 boolean isRunningInEmualtor() { boolean qemuKernel = false; Process process = null; ...
- Java并发编程的艺术(七)——Executors
Executors框架简介 Executor框架便是Java 5中引入的,其内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动.执行和关闭,可以简化并发 ...
- Bootstrap table的基本使用总结
最近在学习BootStrap构建页面,现记录BootStrap table 的一些基本使用如下: HTML文件: <!DOCTYPE html> <html> <meta ...
- [Linux] ubuntu各目录含义
/boot/: 启动文件,所有与系统启动有关的文件都保存在这里 /boot/grub/:grub引导器相关的配置文件都在这里 /dev/:此目录中保存了所有设备文件,例如,使用的分区:/dev/hda ...
- MongoDB单机, 主从, 分布式部署
MongoDB是最易用的NoSQL,比较适合取代MySQL做一些存储,不过不是强一致性的.本文介绍一下MongoDB各种部署方式,并分享一些感受.前两部分“单机部署”和“主从部署”是“分片部署”的基础 ...
- Swift3.0:照片选择
一.介绍 图片选择或者拍照功能: 1.选择相册中的图片或是拍照,都是通过UIImagePickerController控制器实例化一个对象,然后通过self.presentViewController ...
- 第三十一章 elk(2)- 第二种架构(最常用架构)
参考:http://linuxg.blog.51cto.com/4410110/1761757 最常用架构: 一.安装redis 1.下载:http://redis.io/download 2.解压后 ...
- FileStream 的FileShare一点小认识
C#读写文本文件一般都是用StreamWriter来实现(读书的时候就这样用,毕业后这几年基本也是这样干的),通常代码如下: using (StreamWriter sw = new StreamWr ...
- AOP AspectJ 字节码 示例 Hugo MD
Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...
- axios post提交的Content-Type
使用axios的坑 jQuery.ajax的post提交默认的请求头的Content-Type: application/x-www-form-urlencoded而axios.post提交的请求头是 ...