安全工作一直是我们日常开发中需要注意的一个问题,对于 Web 开发而言,需要引起我们重视的主要就是 JavaScript 的安全性了。JavaScript 这样一种脚本语言可以运行在各种浏览器中,但是基于安全性的考虑,几乎所有的浏览器提供给 JavaScript 的接口都是很有限的,尤其是一些安全敏感的接口,如文件的读写操作,内存的控制等等。
这么看似乎 JavaScript 不论怎样写都是非常安全的,其实不然。即便是在这样一个限制重重的环境里,一样有很多漏洞可钻,比如:当您的网站引入了一个外网的 JavaScript,就很容易造成变量的冲突(尤其是全局变量),DOM 操作的混乱等等。如果不对该 JavaScript 作一些限制,那么我们是很难保证自己的网站不会被一些外界因素所干扰,甚至崩溃也不是没有可能。Dojo 的安全工具包提供了一组接口 API 专门用于解决这样一类安全问题,基于这组安全接口,我们不再需要自己额外加上一些安全方面的控制代码。这篇文章将重点介绍 Dojo 的这套安全工具包的功能和使用方式。
Dojo 的 Secure 工具包简介
当我们的应用需要和一些外部的脚本或者数据协同工作时,我们不可避免的要为保证我们自己代码和数据的安全而加入一些额外校验或检测代码。很多情况下,我们不一定能考虑到所有可能的情况,所以我们的应用会存在或多或少的安全隐患。于是,Dojo 的 Secure 工具包应运而生。Dojo 的 Secure 工具包主要包括一些日常经常需要用到的安全相关的一些工具,它封装了很多安全相关的 API,我们只需要通过简单的调用即可完成我们安全检测。
Capability 接口
Dojo 的 Secure 工具包中的 Capability 接口主要用来验证 JavaScript 脚本的合法性,以保证在脚本执行之前确保该脚本不会访问或修改它权限之外的对象或数据。它的使用方式很简单:
dojox.secure.capability.validate(script,safeCalls,{document:1,element:1}) var safeCalls = ["isNaN","isFinite","parseInt","parseFloat","escape","unescape","encodeURI","encodeURIComponent","decodeURI","decodeURIComponent","alert","confirm","prompt","Error","EvalError","RangeError","ReferenceError","SyntaxError","TypeError","Date","RegExp","Number","Object","Array","String","Math","setTimeout","setInterval","clearTimeout","clearInterval","dojo","get","set","forEach","load","evaluate" ];这里主要用到的是“dojox.secure.capability”对象的“validate”方法,第一个参数就是将要执行的脚本字符串,
第二个参数“safeCalls”表示允许的安全函数或对象,它在接下来的代码中定义了,这里像“isNaN”,“escape”,“confirm”,“setTimeout”等等都被看作是安全的,而“parent”,“__parent__”,“__proto__ ”,“with”,“caller”这里是没有的,它们都有很大的安全隐患,即不推荐使用。如果您的 JavaScript 脚本里面调用了“safeCalls”以外的方法会,被检测为不合法。
第三个参数是定义允许的全局变量,这里的“document”和“element”是被允许的,其它均为不合法。
所以,在我们引入外部第三方的 JavaScript 脚本时,可以先验证一下,确保安全再执行。
当然,它也有一些通用的检验规则:
这些各种规则都会作为 Dojo 的安全检测因子,以保证我们引入的外部第三方 JavaScript 脚本代码不会对我们的应用造成任何影响。
DOM 相关接口
接下来我们来看看 DOM 相关的安全接口。Dojo 的 Secure 工具包提供了一些对象可以帮我们自动检测到我们做的 DOM 操作中不合法的一些语句,并抛出相应的异常。基于这些接口和对象,我们可以放心的进行 DOM 结构相关的开发,将检测的工作扔给 Dojo,我们所要做的仅仅是加入自己的异常处理代码即可。
var div = document.createElement("div"); document.body.appendChild(div); div.innerHTML = "SandBoxed div:"; div.style.position = "absolute"; div.style.top = "100px"; div.style.left = "100px"; div.style.backgroundColor = "red"; div.style.color = "white"; var container = document.createElement("div"); container.style.backgroundColor = "cyan"; container.style.color = "black"; div.appendChild(container); wrap = dojox.secure.DOM(container); securedElement = wrap(container); console.log("securedElement",securedElement); securedDoc = securedElement.ownerDocument; console.log("securedDoc",securedDoc);这里我们简单地构造了一个 DOM 结构,并将其包装为“安全”的 DOM 树:“dojox.secure.DOM(container)”,这个时候,我们可以通过“securedDoc ”对象来进行接下来的操作(注意它的取值:“securedElement.ownerDocument”)。
我们来看几个合法的示例:
securedElement.innerHTML = "Hi there"; t.assertEqual("Hi there",securedElement.data__.innerHTML); securedDoc.write("<div style='color:red'>written</div>"); securedDoc.close(); t.t(securedElement.data__.innerHTML.match(/written/)); var newDiv = securedDoc.createElement("div"); newDiv.innerHTML = "inner div"; newDiv.style.color="blue"; securedElement.appendChild(newDiv); t.t(securedElement.data__.innerHTML.match(/inner/)); securedElement.addEventListener("click",function(event) { alert('proper click handler'); });这里列出了一些合法的操作,如“innerHTML”,“write”,新建 DIV 以及关联事件等,操作方式和正常 DOM 操作无异。但是,一旦里面有一些不安全的操作或者数据,便会有异常抛出,我们看几个不安全的示例就了解了:
t.f(securedElement.parentNode);很简单,“parentNode”在安全 DOM 操作中是不允许的,原因就是它可能会破坏上层节点,而其上层节点很可能不在它的允许范围之内。
还有针对“script”标签的不安全操作:
try { securedElement.innerHTML = "<script>bad=true</script>"; }catch(e){} t.t(typeof bad == 'undefined'); try{ securedElement.innerHTML = '</script><script>bad=true;//'; }catch(e){} t.t(typeof bad == 'undefined'); try{ securedDoc.write("<script>bad=true;</script>"); }catch(e){} t.t(typeof bad == 'undefined'); try { var script = securedDoc.createElement('script'); script.appendChild(securedDoc.createTextNode( 'bad=true')); securedElement.appendChild(script); } catch(e) {} t.t(typeof bad == 'undefined');脚本“bad=true”没有“var”,是不安全的全局变量。
Script 标签本身写法就不安全。
通过“write”方法也逃不过安全检测。
通过构造节点的方式也是行不通的。
针对 CSS,Dojo 的 Secure 包也会做安全检测:
if (dojo.isIE) { securedElement.innerHTML = '<div id="oDiv" style="left:expression((bad=true),0)">Example DIV</div>'; t.t(typeof bad == 'undefined'); } else { try{ securedElement.innerHTML = '<input style=\'-moz-binding: url( "http://www.mozilla.org/xbl/htmlBindings.xml#checkBox");\'>'; }catch(e){} t.f(securedElement.innerHTML.match(/mozilla/)) } if (dojo.isIE) { securedElement.style.left = 'expression(alert("hello"),0)'; t.f(securedElement.style.left.match(/alert/)); } else { try { securedElement.style.MozBinding = 'url("http://www.mozilla.org/xbl/htmlBindings.xml#checkBox")'; }catch(e){} } if (dojo.isIE) { securedElement.style.behavior = 'url(a1.htc)'; t.f(securedElement.style.behavior); }Dojo 认为节点的 CSS 样式中含有如下操作符的都是不安全的:“behavior:|content:|javascript:|binding|expression|@import”。这也很合理,因为这里的 CSS 操作符对整个页面都会产生影响。由于清单 6 的例子中含有“expression”,“binding”,“behavior”等,所以,它们都会抛出异常。
接下来,我们来看看它的关于事件绑定的安全检测:
securedElement.innerHTML = "<a href='javascript:alert(3)'>illegal link</a>";
try{ securedElement.innerHTML = "<div onclick='alert(4)'>illegal link</div>"; }catch(e){} t.f(securedElement.innerHTML.match(/alert/));以上的两种方式都是有安全隐患的,Dojo 会判定其为不安全脚本并抛出异常。
最后,Dojo 还会拦截不规则的 HTML:
try { securedElement.innerHTML = '<div x="\"> <img onload=alert(42)src=http://json.org/img/json160.gif>"></div>'; }catch(e){} t.f(securedElement.innerHTML.match(/alert/)); try { securedElement.innerHTML = '<iframe/src="javascript:alert(42)"></iframe>'; }catch(e){} t.f(securedElement.innerHTML.match(/alert/)); try{ securedElement.innerHTML = '<iframe/ "onload=alert(/XSS/)></iframe>'; }catch(e){} t.f(securedElement.innerHTML.match(/alert/));这里的三个示例显而易见都是不规范的 HTML,针对这些情况 Dojo 会这届抛出异常。可能有人觉得通过 HTML 的容错机制将其“翻译”成正确的 HTML 即可,但是这种方法往往会有“翻译”错误的情况,并且其中的一些特殊标记可能会造成更大的安全隐患。
JSON 相关
在 JavaScript 中 JSON 数据的处理也是 Web 开发中十分重要的一环,所以 JSON 数据处理的安全也尤为重要。Dojo 的 Secure 工具包提供了安全处理 JSON 数据的接口:
var i,mediumDataSet = []; for(i = 0; i < 20; i++){ mediumDataSet.push({ prop1: null,prop2: true,prop3: false,prop4: 3.4325222223332266 - i,prop5: 10003 + i,prop6: "Lorem ipsum dolor sit amet,consectetuer adipiscing elit. Aenean semper",prop7: "sagittis velit. Cras in mi. Duis porta mauris ut ligula. Proin porta rutrum",prop8: "lacus. Etiam consequat scelerisque quam. Nulla facilisi. Maecenas luctus",prop9: "venenatis nulla. In sit amet dui non mi semper iaculis. Sed molestie",prop10: " tortor at ipsum. Morbi dictum rutrum magna. Sed vitae risus." + "Aliquam vitae enim." }); } var mediumJson = dojo.toJson(mediumDataSet); // 安全 JSON 处理 dojox.secure.fromJson(mediumJson); // 不安全 JSON 处理 dojo.fromJson(mediumJson);这里我们构造了一个 JSON 数据,并给了两种 JSON 的数据处理方法:“dojox.secure.fromJson”和“dojo.fromJson”,为什么前者安全?答案很简单:“dojo.fromJson”是直接调用“eval”来解析 JSON 数据,“dojox.secure.fromJson”是通过字符分析来解析的。大家都知道“eval”是很不安全的,而且它的效率低下。试想一下,如果我们并不知道拿到的数据是否严格符合 JSON 的格式,如果它就是一串 JavaScript 脚本代码,我们这里不假思索的调用“eval”将会产生怎样的后果?所以,这里强烈建议使用安全的方法:“dojox.secure.fromJson”。
SandBox
最后,我们来看看 SandBox(沙箱),这是一种可以安全运行外部对象的模式,尤其是跨域取得的页面,JSON,JavaScript 脚本等等。Dojo 的 Secure 提供了这种接口,类似于“eval”,不同的是它可以保证被执行的代码不会对您已有的应用造成任何安全问题。
var div = document.createElement("div"); document.body.appendChild(div); div.innerHTML = "SandBoxed div:"; div.style.position = "absolute"; div.style.top = "100px"; div.style.left = "100px"; div.style.backgroundColor = "red"; div.style.color = "white"; container = document.createElement("div"); container.style.backgroundColor = "cyan"; container.style.color = "black"; div.appendChild(container); // 安全 dojox.secure.evaluate("element.innerHTML = 'Hi there';",container); t.assertEqual("Hi there",container.innerHTML); dojox.secure.evaluate(" document.write(\"<div style='color:red'>written</div>\");",container); t.t(container.innerHTML.match(/written/)); // 不安全 t.f(dojox.secure.evaluate("document.body",container)); try { dojox.secure.evaluate("bad = true",container); }catch(e){} t.t(typeof bad == 'undefined');这里我们在页面上构造了一个 DOM 结构,并分别执行了相应代码,注意:这里按照沙箱的模式给予安全限制:您所执行的任何代码都是“与世隔绝”的,即您是不可能影响到“container”以外的任何节点和脚本的。基于这些限制因素,我们这里的“element.innerHTML”和“document.write”都是安全的,而“document.body”(页面 body 节点)和“bad = true”(全局变量)都是不安全的,因为它们可能会对“container”以外的事物造成影响。
再来看看如何跨域执行代码,在保证安全的的情况下:
var sandBox = dojox.secure.sandBox(document.getElementById("sandBox"));
// 本地模式 try{ sandBox.evaluate(input); }catch(e){ alert(e.message || e); } // 跨域模式 var input = document.getElementById("jsFile").value; sandBox.loadJS(input).addErrback(function(result){ alert(result); }); var input = document.getElementById("htmlFile").value; sandBox.loadHTML(input).addErrback(function(result){ alert(result); });如上所列,本地模式的接口很简单:“sandBox.evaluate”,和之前的基础接口无异。跨域模式则是基于异步模式的调用,这里您只需要通过“addErrback”添加异常处理代码即可。注意,这里的“loadJS”和“loadHTML”分别用于执行 JavaScript 和 HTML 代码,它们的传入值应为一段 URL,如:“http://www.sitepen.com/labs/code/secure/dojox/secure/tests/good.js” 或 “http://www.sitepen.com/labs/code/secure/dojox/secure/tests/good.html”。
结束语
这篇文章介绍了 Dojo 中安全工具包的一些特性,从安全的角度阐述了 Dojo 的安全工具包的各种借口,以及我们为什么需要这些接口。先介绍了 Capability 接口,即如何检测我们代码的安全性。进而扩展到 DOM 和 JSON 相关的接口,即哪种操作才是安全的。最后,通过 SandBox 接口介绍了在本地和跨域两种情况下,如何通过 Dojo 的接口安全的执行相关代码。本文主要是基于实际的代码示例来说明这些接口的用法,简明直观,推荐大家在日常开发中多参考。
本文首发于IBM Developerworks:http://www.ibm.com/developerworks/cn/web/1204_zhouxiang_dojosecure/