使用jsp、php、asp或者后来的struts等等的朋友,不必定知道什么是模版,但必定很清楚这样的开发方式:javascript
<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模版。php
在HTML5时代,咱们更多使用前端资源静态部署,更多场景下须要使用前端模板库把后台返回的JSON数据填充到页面中。前端使用模版库,比手工拼接字符串要优雅不少。前端
固然若是后端使用nodejs,前端模版库或者叫js模版库同样能兼容使用。java
这里拿一个很是简洁的模版库做为介绍,做者John Resig也就是鼎鼎大名的jQuery创始人。代码只有聊聊可数的十几行:node
// 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; }; })();
关键是三部分:jquery
首先看一个使用例子,从使用的例子慢慢解剖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逻辑。app
那么接下来,咱们深刻看看模版tmpl函数里边到底作了什么?
一、看看最终生成的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接受若干个参数,最后一个参数就是函数体字符串,前边的都是参数名。
关键是红色部分,这部分就是那些很是“艺术”的正则匹配替换,最终获得的字符串。
二、逐步看看正则表达替换是如何运做的
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的地方有两个:
因为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更完善一些,它提供了两种注入数据的方式:
固然,咱们也能够把第一种模式也作成自动转义,正如我如今项目就须要这么搞。。。大概就是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";