【超精简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";
时间: 2024-11-05 09:41:56

【超精简JS模版库/前端模板库】原理简析 和 XSS防范的相关文章

HTML5前端入门教程:简析正则表达式

很多人对正则表达式的印象都是用来做表单验证的,这其实是不大准确的.正则表达式目前在很多软件中都得到了广泛的应用,包括Linux,Unix等操作系统,VB,Java,PHP等开发环境中,以及很多应用软件都能应用到正则表达式. 一.正则的历史 首先先来扫清一个误区,老是有人认为正则表达式是JS自己发明的,这当然是不正确的.1956年,一位名叫Stephen Kleene的数学家在McCulloch和pitts早期工作的基础上,发表了一篇标题为<神经网的表示法>的论文,第一次引入了正则表达式的概念.

互联网我来了 -- 2. js中&quot;异步/阻塞&quot;等概念的简析

一.什么是"异步非阻塞式"? 这个名字听起来很恶心难懂,但如果以 买内裤 这件事情来比喻执行程序的话就很容易理解"异步非阻塞式"的涵义了. 例如你是一个CPU的线程,你需要去执行一段 买内裤的程序, 你所需执行的步骤大致如下, 到一个商店里问老板, 你们店里还有没有nb牌内裤? 买到内裤,穿上 去小卖店买点火腿回家喂狗 这时候,你作为一个线程,你可能会遇到几种状况或选择. 店里面没货了,老板一直不答应你(阻塞你),你也一直等着(同步),第三天有货了才告诉你有了,你赶

搭建前端组件库(一)

本文梳理如何搭建和构建前端组件库. 了解几个问题 为何需要组件化? 大部分项目起源都是源于业务方的各种各样的奇葩需求.随着公司的业务发展,公司内部开始衍生出很多的B2C系统.后台系统,前端部门也疲于应对越来越多同质化的项目,这些项目在很多基础模块层.源代码存在不小的相似,甚至存在相似的业务模块. 笔者曾经所在的一个电商团队,前端成员基本每个人多做过登录注册.购物车.支付.微信登录...... 大量重复的业务代码.由于组内技术没有强制规范 本质上相同的东西,重复的去code就显得浪费. 分析这些问

南京都昌科技电子病历模板库清单

2015年1月30号南京都昌科技电子病历模板库有1074个模板文件,清单如下,有意购买者请致电13382028281南京都昌科技市场部刘经理. 文档模板库\SG\南京都昌科技2_24小时内入出院记录.xml文档模板库\SG\南京都昌科技2_24小时内入院死亡记录.xml文档模板库\SG\南京都昌科技2_上级查房记录.xml文档模板库\SG\南京都昌科技2_会诊记录.xml文档模板库\SG\南京都昌科技2_入院记录.xml文档模板库\SG\南京都昌科技2_出院记录.xml文档模板库\SG\南京都昌

标准模板库

-------------------siwuxie095 在长期的编码中,聪明的程序员们发现:有一些代码经常碰到, 而且需求特别稳定,于是,各大公司在出售自己的 IDE 环境时, 就会把这些模板代码打包,一起销售 慢慢地,这些大公司之间就达成了某种共识,觉得应该把这些 涉及模板的通用代码进一步的统一和规范,于是,大家慢慢形 成了一套 C++ 的标准模板,就是现在所看到的标准模板库 标准模板库 标准模板库,即 Standard Template Lib,简称为 STL 标准模板库所涉及的内容非常

前端与编译原理 用js去运行js代码 js2run

# 前端与编译原理 用js去运行js代码 js2run 前端与编译原理似乎相隔甚远,各种热门的框架都学不过来,那能顾及到这么多底层呢,前端开发者们似乎对编译原理的影响仅仅是"抽象语法树",但这只是个开头而已,我们的目的是利用js直接运行js代码 项目地址 安装及使用方法 写这个干嘛,有现成的eval不香么 接触过微信小程序开发的同学或许知道,小程序为运行环境禁止new Function,eval,setTimeout等方法的使用,限制了我们执行字符串形式的动态代码,其他小程序平台对此也

前端组件库大合集-必备收藏

前端组件库 搭建web app常用的样式/组件等收集列表(移动优先) 0. 前端自动化(Workflow) 前端构建工具 Yeoman – a set of tools for automating development workflow gulp – The streaming build system grunt – the JavaScript Task Runner F.I.S – 前端集成解决方案 前端模块管理器 Bower – A package manager for the w

C++ primer plus读书笔记——第16章 string类和标准模板库

第16章 string类和标准模板库 1. string容易被忽略的构造函数: string(size_type n, char c)长度为n,每个字母都为c string(const string & str, size_type pos = 0, size_type n = pos)初始化为str中从pos开始到结尾的字符,或从pos开始的n个字符 string(const char *s, size_type n)初始化为s指向的前n个字符,即使超过了s的结尾: string(Iter b

前端开发框架库 zeptojs 和 avalon

前端开发库 zeptojs 和 avalon    迷你MVVM框架 avalonjs 入门教程 针对zepto的扩展  GMU  有很多UI控件可以直接用. WEB里面的项目大多基于express搭建 学习Express框架 Handlebars.js 模板引擎 推荐学习资料链接: JavaScript 标准参考教程(alpha)