Javascript安全那些事-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

Javascript安全那些事

简介:

这篇文章有点长,如果你对JavaScript安全感兴趣,请耐心看下去

大纲

  • 业务背景
  • JavaScript相关安全框架剖析
  • Why Caja
  • 现状与将来
  • 总结

背景

目前淘宝店铺系统,u站,品牌站,爱淘宝等业务,都开放第三方ISV/设计师在我们系统中编写JavaScript代码,用来完成动态效果和一些交互特性。

Caja就是用来保证这个第三方在我们系统中编写JavaScript安全性的一个前端基础技术方案。

目前两个典型的业务场景

ISV的代码运行在我们系统内部

  • 在我们系统中完全禁止ISV编写JavaScript, 这也是淘宝旺铺模板的早期版本。 不写JS当然不会有这些问题。 所带来的问题是,模板的效果有限,无法更加个性化

ISV代码运行在我们外部

  • 比如淘宝开放平台,聚石塔我们需要开放很多API给外部ISV,让ISV可以根据这些API给卖家提供更多好用的工具。 而应用本身可以host在外部,这样其实我们不需要担心淘宝cookie被盗用这些问题,但是很难避免的一点是,数据外泄。举个例子,ISV为淘宝卖家开发了一个分析销售数据的报表系统,那么可以很容易的知道,ISV只要通过服务端开放API分析报表的结果,通过JS一个http请求,就可以把数据发送到自己的服务器上去。并且,事实上,确实目前外面很多ISV通过这种方式获得了很多我们的数据。

JavaScript安全框架剖析

Caja 是google开发和维护的一个前端安全框架,专注于第三方接入的安全。我们为什么最终选择了它?

因为JavaScript是一门动态的,解释性语言,并且我们也很难控制到JavaScript运行的虚拟机这个层面,这让实现一套JavaScript的安全机制变得很困难,但业界也有几个公司做过尝试,我们可以稍微剖析一下。

首先,为什么第三方在我们系统中使用JavaScript是危险的,主要包含以下几个方面

  • Cookie
  • 数据外泄
  • 钓鱼链接
  • 让整个页面不可用
  • 跳转
  • ...等等

可以说,如果第三方的JavaScript在我们系统中不受任何控制,那么就没有任何安全可言。

所以,如何让外部编写的JavaScript语言可以安全的运行在我们的系统中,这是一个研究的话题。

一个语言的安全策略,我们要想控制,通常是两中思路

  1. 修改语言本身的代码(编译)
  2. 修改运行时环境

比如Java语言,我们可以修改其虚拟机的运行环境,上下文,来达到对里面net等这些接口的控制
在NodeJs中,也有个VM模块 用来控制虚拟机的上下文。

这些都是很好的思路,很可惜,JavaScript的运行时环境,我们是没办法控制的,因为浏览器是运行在用户客户端的。这就需要走另外一个路线,修改语言代码本身,也就是编译。

我们知道,一个语法解析器目前实现起来是比较容易的,目前JavaScript这个 原理不用多说,就是jison 这就让我们想到了,能否对ISV上传的JavaScript代码做一些检查?

ps: 以下部分来自于blog

FBML

这是facebook的一个安全框架,目前已经不维护了。他的主要思路就是对用户的Javascript代码进行编译。

function $(str) {
    return document.getElementById(str);
}

$("hello");

上传后变成了

function a1_$(a1_str) {
    return a1_document.getElementById(a1_str);
}

a12_$("hello");

这样做的好处是第三方的js不会影响到页面中其它js,它只能调用自己内部创建的变量及函数,为了让它具备一定的功能, 可以在运行前给它提供一些全局对象,如前面用到了document全局对象,经过转换后变成了a1_document,只要在运行第三方js前先对这个对象进行赋值,第三方的js就能使用了

var a1_document = new fbjs.main('1');

这样,在fbjs.main对象就代替了第三方js中的document对象,在这里就能进行各种检查,保证第三方js不会影响到页面的其它部分,而且做到了白名单机制,第三方只能调用几个特定的api,而不能进行读写cookie等操作,如fbjs_main实现的一个简单示例

function fbjs_main(appid) {}

fbjs_main.prototype.getElementById = function(id) {
    return fbjs_dom.get_instance(document.getElementById(id));
}

可以看到,实际上第三方调用document.getElementById返回的是一个内部的对象,而不是实际的dom节点,这样的好处是可以限制第三方程序的运行,缺点是不能直接访问很多dom节点的属性(如tagName),只能通过调用相应的方法来进行读写,有点类似于jQuery

FBML的转换库是开源的,感兴趣的同学可以去libfbml看看,它使用了firefox 2.0.0.4源码中的函数来解析html、css、js

除了解析和代码转换,由于js的动态性,使得很多问题不能通过静态分析发现,因此还需要在运行时进行安全检查,运行库是fbjs,它的实现在fbjs.js中

接下来我们就具体研究一下FBML是如何保证安全的(主要JavaScript部分)

保证js安全主要分为两方面,一是静态分析,如禁止某些函数和变量名加前缀,另一个是进行运行时的检查,如this引用

this引用

this很容易就指到windows上,如在新建对象时少写了个new

function Car() {
    this.xx = 'yyy';
}
var car = Car();
alert(xx)

所以需要对this进行重新封装,在运行时对其进行检查,将上面的代码转成

function a1_Car() {
    ref(this).xx = 'yyy';
}
var a1_car = a1_Car()

这样就能在运行时对其进行检查,避免它指到window上,ref函数的实现是

function ref(that) {
    if (that == window) {
        return null;
    } else if (that.ownerDocument == document) {
        fbjs_console.error('ref called with a DOM object!');
        return fbjs_dom.get_instance(that);
    } else {
        return that;
    }
}

with

with无法进行静态分析,因为它临时插入了一个上下文,需要在运行时才能知道取得的变量是哪个,所以FBML中将其禁止了,后面的很多方案也都禁止with语句

危险的属性

js中的某些属性很危险,如可以通过constructor拿到生成对象的构造函数,如下面的代码可以修改Object函数

var o = {};
o.constructor.prototype.xx = 'xx';

在FBML中将如下几个属性都替换成__unknown__

proto
parent
caller
watch
constructor
defineGetter
defineSetter

然而由于js的动态性,还可以使用方括号语法来获得这些属性,如之前的代码可以等价于

var o = {};
o['constr' + 'uctor'].prototype.xx = 'xx';

静态分析是无法发现这类问题的,所以需要类似this那样,对方括号内的属性加上一层运行时的检查

function idx(b) {
    return (b instanceof Object || fbjs_blacklist_props[b]) ? '__unknown__' : b;
}

不过上述列表其实还是有风险的,后面的caja、ADsafe等都直接禁止后缀带_的属性,避免后续浏览器升级导致的漏洞

arguments

在老版本的Firefox和IE中,arguments对象可以通过caller取到调用当前函数的函数,会带来很多安全隐患,如ajax回调第三方函数时就把ajax库给暴露了,所以FBML给它加了一层封装,转成arg(arguments),arg函数会将其转成普通数组

function arg(args) {
    var new_args = [];
    for (var i = 0; i < args.length; i++) {
        new_args.push(args[i]);
    }
    return new_args;
}
eval

因为eval没法进行分析,只能将其禁止,对于需要获取json数据的第三方js,由fbjs提供了Ajax库来转成json对象,不过从代码看这里并没用做相应的安全检查,会造成安全隐患

为了保证eval第三方json时的安全,可以采用json2.js的做法,在eval前检查是否是合法的json
if (!/^[\],:{}\s]*$/.test(data.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@")
    .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]")
    .replace(/(?:^|:|,)(?:\s*\[)+/g, "")) ) {
    return null;
}

去掉注释

去掉注释似乎是为了减少大小,然而由于IE的条件编译机制,导致注释里还能执行任意JS,所以注释必须去掉,如

/*@cc_on @*/ /*@if (1) alert(document.cookie) @end @*/

array中的很多方法

在某些浏览器下,array中的很多方法通过call和apply调用时会返回window对象,如下写法在Firefox、Chrome等浏览器中会取到window对象

alert(window === ([]).sort.call());

所以在fbjs中对这些方法都进行了重写,避免它在运行时的this指向window

Array.prototype.sort = (function(sort) { return function(callback) {
    return (this == window) ? null : (callback ? sort.call(thisfunction(a,b) {
        return callback(a,b)}) : sort.call(this));
}})(Array.prototype.sort);

甚至还将reduce、reduceRight直接删掉了

Array.prototype.reduce = null;
Array.prototype.reduceRight = null;

DOM
为了让第三方js能对页面元素进行控制,fbjs封装了dom的很多方法,并对其进行运行时的检查

getElementById

因为id都加上前缀了,所以fbjs提供给第三方js的api需要加上前缀才能取到

fbjs_main.prototype.getElementById = function(id) {
    var appid = fbjs_private.get(this).appid;
    return fbjs_dom.get_instance(document.getElementById('app'+appid+'_'+id),appid);
}

getParentNode

为了不让第三方js影响到页面的其它部分,需要在dom节点移动时进行检查,如获取parentNode,要将其限定到第三方应用所属的容器里,不能取高于这个节点的其它元素

createElement、innerHTML
fbjs提供了createElement接口来让第三方js创建元素,并进行检查,只允许创建一些安全的元素

而对于很多开发人员喜欢使用的innerHTML则不容易进行安全检查,因为需要在运行时对html进行解析,fbjs目前只提供两种方式,一种是setInnerFBML,通过FBML中的一个自定义标签fb:js-string,让FBML来解析,它的缺点是无法在运行时拼装,另一种是setInnerXHTML,它要求传递的字符串是一个合法的xml形式,直接使用浏览器的XML解析器来解析

location、src、href
有一种风险是可以动态创建一个a标签,设置它的href来生成x,所以这些url的属性都需要加上一层判断

fbjs_dom.href_regex = /^(?:https?|mailto|ftp|aim|irc|itms|gopher|\/|#)/; 

fbjs_dom.prototype.setHref = function(href) {
    href = fbjs_sandbox.safe_string(href);
    if (fbjs_dom.href_regex.test(href)) {
        fbjs_dom.get_obj(this).href = href;
        return this;
    } else {
        fbjs_console.error(href+' is not a valid hyperlink');
    }
}

除此之外还有img的src和location.href

setTimeout、setInterval
setTimeout、setInterval是可以传递字符串来执行的,和eval一样没法进行静态分析,如

var a = 'ale';
var b = 'rt()';
setTimeout(a+b, 10);

于是fbjs对这两个函数都进行了封装,限制其只允许传递函数类型

fbjs_sandbox.set_timeout = function(js, timeout) {
    if (typeof js != 'function') {
        fbjs_console.error('setTimeout may not be used with a string. Please enclose your event in an anonymous function.');
    } else {
        return setTimeout(js, timeout);
    }
}

js代码的执行
fbml解析后的js并不直接放回

function eval_global(js) {
    var obj = document.createElement('script');
    obj.type = 'text/javascript';
    try {
        obj.innerHTML = js;
    } catch(e) {
        obj.text = js;
    }
    document.body.appendChild(obj);
}

为何要这么做呢? 我能想到的一个原因是js字符中script标签会导致漏洞,不好进行静态分析,如下面的代码会在IE、Chrome下执行alert


<script>
var a = "</script><script>alert()</script>";
</script>

不过因为FBML使用了firefox的引擎来解析html,所以直接在字符串中第一个标签就截断了

隐蔽的问题
了解fbml的解决方案后,感觉挺似乎很完美,然而实际上远没有那么简单,Facebook script injection vulnerabilities上报了很多Facebook的漏洞,如Firefox中支持的E4X语法,因为Facebook是基于firefox2的解析引擎,所以导致解析时没有发现问题

<script>
<x x="
x" {alert('any javascript')}="x"
/>
</script>

在fbjs中有大量这样case by case的漏洞修复,涉及到浏览器特性的问题是无法预测的,尤其是浏览器解析时对html的容错机制(这也是html5重点解决的一个问题,感兴趣的同学可以看看其中的第8章),不过总的来说fbml的解决方案还是很不错的,也经受了大量的线上考验

Microsoft Web Sandbox

Microsoft Web Sandbox是微软提出的一种方案,它最大的特点就是引入了虚拟机的思想,将代码全部转成了虚拟机中的调用,将很多安全检查的工作放到运行时去解决, 在这个虚拟机中将所有html标签,以及所有的js对象和dom的调用都进行了封装,将第三方代码完全和外部环境隔离开

实现细节
我们来看看它的具体是怎么实现的,首先html源码经过转换后全都成了js变量,如下html

<html>
    <body>
        <a id="b" href="javascript:alert()">click</a>
    </body>
</html>

经过转换后会变成

var settings = {};

var headerJavaScript =
function(a)
{
    var b = a.gw(this), //获取虚拟机内部的全局对象
        c = a.g,        //用于取得对象的属性,如后面用来取document
        d = a.i,        //用于调用一个方法,如后面用来调用alert
        e = a.f,        //用于将函数封装起来,对函数的运行做监控,如避免死循环和频繁alert等问题
        f = c(b,"document");
    //接下来通过调用initializeHTML来生成html
    d(f,"initializeHTML",[[{"body":{"c":[," ",{"a":{"a":{"href":e(function()
    {
        d(b,"alert")
    }),"id":"b"},"c":[,"click"]}}," "]}},{"#text":{"c":[" "]}}]])
}; 

var metadata = {"author":"","description":"","imagepath":"","title":"","preferredheight":0,"preferredwidth":0,"location":"","icon":"","base":{"href":"","target":""}}; 

$Sandbox.registerCode(headerJavaScript, "1", settings, metadata); 

var SandboxInstance = new $Sandbox(document.getElementById('g_1_0_inst'), $Policy.Canvas, "1"); 

SandboxInstance.initialize();

对于html和css,都进行了Tokenization,转成了json的形式,注意这里并没有像fbml那样进行代码转换,而是等到了运行时再拼装,如将

    <a id="b" href="javascript:alert()">click</a>

转成了json形式的变量

{"a":
    {
        "a":{
                "href":e(function() {
                        d(b,"alert")
                }),
                "id":"b"
            },
        "c":[,"click"]
    }
}

在最后由运行库来拼接成html,如

    <a id="cst9" href="javascript:$Sandbox.Instances['__S__8'].__exechref__(0)">click</a>

这样的好处是能轻松迁移和多实例,因为生成后的代码相当于虚拟机的指令,而不像fbml那样将都和特定id绑死了,websandbox能同时运行几个相同的第三方代码,只需再new一个$sandbox就可以了

websandbox的虚拟机机制使得让它能进行大量的运行时检查,这样就能解决死循环和无限alert的问题,这都是其它方案无法解决的,也是目前这几种方案中功能最强大的,可惜解析部分并没有开源,只将运行库开源了,解析只能手工执行

ADsafe
和前面几种方案不同,Douglas Crockford写的ADsafe
采用了另一种方式,它并没有进行代码转换,而是采取了验证的方法,第三方的JS需要先经过jslint的验证才能允许执行,这些验证包括

不得使用除了ADsafe提供以外的任何全局对象,对于Array Math等提供了有限的范围
不能使用this arguments eval with
不能使用arguments callee caller constructor eval prototype stack unwatch valueOf watch
不能使用以_开头或结尾的string
不能使用[]
不能使用Date和Math.random,这样保证widget的运行不会有随机性
ADsafe不进行代码转换避免了很多运行时的性能开销,然而却导致了大量的限制,第三方开发人员有一定的学习成本,如没法使用[],而必须使用ADSAFE.get ADSAFE.set

不过ADsafe最大的好处就是可以不依赖后端,很适合做简单的widget及嵌入第三方的广告,管理员只需要将第三方js代码拿到jslint中验证一下即可,不了解js也能进行审核,而且因为禁止了Date和Math.random,使得js代码的执行结果是稳定的,下面演示了一个简单例子

<div id="WIDGETNAME_"><input type=text /> </div>
<script>
"use strict";
ADSAFE.go("WIDGETNAME_", function (dom) {
    var input = dom.q("input_text");
});
</script>

Why Caja

如果你有心已经详细看过上面介绍的几个方案,就拿FBML 来说,你就知道,这是个多么复杂的工程,由于JavaScript语言的动态性以及不同虚拟机的实现会导致很多漏洞的产生,要想解决这些问题你都必须兼顾到各种情况,工程及其浩大。

那么,为什么选择caja?

首先,这不是我选择的方案,是我的老大以及之前一位同事确定的方案。但自从我维护了这么久,我想以下几个优点是目前来看,比较有优势的

  1. caja采用语言编译和运行时环境两种方式的结合,更加牢靠
  2. caja采用的是白名单机制。客户端的api运行时环境被重写后,所有相关的方法都会被转发到运行时的虚拟机,这就让我们对我们需要开放的API都显示的声明,避免不同浏览器的不同实现或者新增的接口带来的安全问题
  3. 接口实现更加细致和完善,就拿 innerHTML 这个方法来说。这个方法一定是可以让ISV用的,但又必须加入我们的白名单校验规则,caja中html,css都是白名单json配置,修改起来比较方便。
  4. 相对完善的属性描述符实现, 关于属性描述符。不用多说了

还有个关键一点,FBML目前也不维护了,也存在不少问题,根本无法作为一个成熟的技术方案,adsafe又限制太多,sandbox也不开源,所以最终可选择的不多。

当然caja的完善也带来了其复杂性。还好目前我们基本掌握的了这个技术和内部原理,可以很好的应用于我们的产品,并且将我们的淘宝前端框架KISSY库注入其中,给外部ISV带来了很大的方便。

现状与将来

其实讨论了这么多,我们都基于一个前提,客户端的浏览器是我们没办法控制的,并且像IE6,7,8这些低级浏览器的存在,让我们少了很多选择

但高级浏览器的新特性,对安全方面给了我们更多的空间

比如**Content-Security-Policy** 介绍
这个特性简单的说就是服务端可以返回一个响应头,告诉客户端浏览器只允许访问哪些url的地址

这个特性非常cool,这样的话,比如我们让ISV开发前端JavaScript代码的之后,至少可以防止数据外泄,cookie盗用,钓鱼等这样的问题,也可以防止ISV自动引入外部的script脚本突破我们的限制。 再配合一些JavaScript的语言分析,就可以解决大部分安全问题。

手机端webview,我们知道webview也就是一个浏览器,只是这个浏览器是可以我们定制的。
我们可以对webview里面的请求或者跳转做监控限制,这个和上面介绍的content-security-policy类似。

如果有部分接口可以让我们对浏览器客户端有一定的安全策略的控制权,那么我们的方案可以重新去考量了,也许caja太重了, caja某些关注的安全问题,其实我们并不关注,未来需要做精简。

总结

文章开头说了一些业务背景,接着介绍了下JavaScript的安全策略方案,最后总结了现状和将来。

从目前来说

pc上买家端 因为存在低版本浏览器,我们没办法控制,caja确实是一个比较好的方案
我们可以指定客户端的浏览器,可以配合一些内容安全策略或者是浏览器插件实现一些“事后监控策略”
对于手机移动端,我们通过定制我们的webview,来达到安全的控制。 

该文章转自阿里巴巴技术协会(ATA)
作者:石霸 

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章
最新文章
相关文章