使用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";