【超精簡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";

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM