Reads:41621Replies:0
On JavaScript security
This article may be a little long. If you are interested in JavaScript security, please read ahead with patience.
Outline • Business Background • Analysis on JavaScript-related Security Framework • Why Caja • Status Quo and Future Trend • Summary Background At present, Taobao’s shop system, U station, brand station, itaobao.com and other businesses are all using third-party ISV/designers to write JavaScript code in our system to achieve dynamic effects and some interactive features. Caja is a front-end foundational technical solution securing the JavaScript code in our system through a third party. There are two typical business scenarios at present. ISV code runs inside our system • In our system, ISV is strictly forbidden from writing JavaScript code in our system, which is also the practice of early versions of wangpu.taobao.com templates. No JavaScript code writing, no such issues of course. But another issue arises: the effects offered by templates are limited and more personalized effects are difficult to implement. ISV code runs outside our system • For example, for the Taobao Open Platform and Jushita, we need to open many APIs to the external ISVs who can therefore provide more useful tools for sellers. But the application itself can be hosted outside our system. In this way, we don’t need to worry about Taobao cookie theft. However, data leakage is hard to prevent. For example, if ISV develops a report system for a Taobao seller to analyze the sales data, it is easy to understand that ISV can send the data to their own server by a JS HTTP request for the open API analysis report results on the server. In fact, many external ISVs did get a lot of our data in this way. Analysis on JavaScript Security Framework Caja is a front-end security framework developed and maintained by Google and focuses on third-party access security. Why did we finally settle on it? Because JavaScript is a dynamic and explanatory language and it is hard for us to control JavaScript virtual servers, which makes it very difficult to implement a JavaScript security framework. But the industry also has some companies who have tried, and we can drill down and analyze their results. First, the reasons that it is dangerous to allow third parties to use JavaScript in our system include the following: • Cookie • Data leakage • Link to a phishing website • Unavailability of the whole page • Navigation • and so on We can say that if we impose no restrictions on JavaScript code writing by a third party in our system, there is no security. So it becomes a topic worth studying regarding how to run a JavaScript language written by an external party securely in our system. To manage the security policies for a language, we usually have two trains of thought: 1. Modify the language code (compiling) 2. Modify the running environment For Java language, we can modify the running environment or context of its virtual machines to control the net interface and other interfaces. In NodeJs, there is also a VM Module in place to manage the VM context. These are all good ideas. But it is a pity that we cannot control the JavaScript running environment because browsers run on the client-side. So we can try another way, that is, modifying, or compiling, the language code. We all know that a parser is easy to implement. At present, regarding JavaScript parsers, enough has been said, that is, the Jison. But it has brought up a new question: can we perform some checks on the JavaScript code uploaded by the ISV? PS: The following content is contributed to by blog FBML This is a security framework by Facebook, but it has ceased maintenance. FBML ensures security through compiling JavaScript code by users. For example: function$(str) { returndocument.getElementById(str); } $("hello"); After the code is updated, it is changed to: functiona1_$(a1_str) { return a1_document.getElementById(a1_str); } a12_$("hello"); One of its advantages is that the third-party JavaScript code won’t affect other JavaScript code on the page, as it can only call the variables and functions created within itself. Some global objects can be provided before code runs to enable some functions, such as the document global object aforementioned which has been changed to a1_document after conversion. As long as we assign a value to this object before running the third-party JavaScript code, the third-party JavaScript code will be usable. var a1_document = new fbjs.main('1'); In this way, the fbjs.main object replaces the document object in the third-party JavaScript code, enabling various checks to ensure that the third-party JavaScript code will not influence other parts of the page. In addition, a white list is also available, in that the third party can only call several specific APIs and is not permitted for cookie read and write. Below is a simple example implemented by fbjs_main: functionfbjs_main(appid) {} fbjs_main.prototype.getElementById = function(id) { return fbjs_dom.get_instance(document.getElementById(id)); } We can see that an internal object returns after the third party calls document.getElementById, instead of the actual DOM node, which helps to restrict the running of third-party programs. But it has a defect that you cannot access attributes (such as tagName) of many DOM nodes directly, but through calling the correct methods for reading/writing, somewhat similar to jQuery. Transformation libraries of FBML are open-source. If you are interested, visit the libfbml which uses functions in Firefox 2.0.0.4 source code for parsing HTML, CSS and JS. In addition to parsing and code transformation, the dynamic characteristic of JavaScript makes it impossible to detect many problems through static analysis. So we have to perform security checks while code runs. The runtime library is fbjs and its realization is in fbjs.js. Next we will study in detail how FBML ensures security (mainly about JavaScript). Two measures are taken to ensure JavaScript code security: one is static analysis, such as disabling some functions and adding prefixes to some variable names, and the other measure is to perform runtime checks, such as this reference. This reference This is easily pointed to windows, due to the absence of a new when a new object is created. function Car() { this.xx = 'yyy'; } var car = Car(); alert(xx) So this should be re-encapsulated and a runtime check should be performed. Change the above code to: function a1_Car() { ref(this).xx = 'yyy'; } var a1_car = a1_Car() In this way, this reference will be checked, avoiding its pointing to Windows. The ref function realization is: 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 Stastic analysis does not apply to the with statement because it has a temporary context inserted. As a result, it is not until runtime that we can know which variable is fetched. Because of this, FBML has disabled the with statement, so do many later solutions. Dangeous attributes Some attributes in JavaScript code are very dangerous. For example, you can get the constructor of the generated object and the code below can modify the object function: var o = {}; o.constructor.prototype.xx = 'xx'; In FBML, the below attributes are all replaced by __unknown__. proto parent caller watch constructor defineGetter defineSetter But because of the dynamic nature of JavaScript code, you can also get these attributes using square brackets. The preceding code is equivalent to: var o = {}; o['constr' + 'uctor'].prototype.xx = 'xx'; Statistic analysis is unable to find such problems. That is why we need to add a runtime check to the attributes in the square brackets, as we do for this reference. function idx(b) { return (b instanceof Object || fbjs_blacklist_props) ? '__unknown__' : b; } But the above list also harbors some risks. Caja and ADsafe in the later part of this article both disable attributes with “_” in the postfix to avoid loopholes resulting from future browser upgrades. arguments In older versions of Firefox and IE browsers, arguments objects can fetch the functions that call the current function through caller, which leads to many potential security hazards, such as AJAX which will expose the AJAX library when calling back third-party functions. So FBML encapsulates it into arg(arguments), and the arg function will convert it to a regular array. function will convert it to a regular array. function arg(args) { var new_args = []; for (var i = 0; i < args.length; i++) { new_args.push(args); } return new_args; } eval The eval cannot analyze but has to disable it. For third-party JavaScript code requesting for JSON data, fbjs provides Ajax library to convert the code into JSON objects. But from the code, we see no security check mechanisms which may lead to potential security hazards. To ensure the security of eval functions for third-party JSON objects, we can adopt the json2.js practice and check JSON validity before running eval. 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; } Comment out It seems that by removing comments, we reduce the size. But the conditional compilation mechanism in IE enables the execution of any JavaScript code in comments. So comments must be removed, as shown below: /*@cc_on @*/ /*@if (1) alert(document.cookie) @end @*/ Methods in array In some browsers, many methods in the array will return Window objects during call and apply calls. For example, the below code will fetch Window objects in Firefox and Chrome browsers: alert(window === ([]).sort.call()); That is why fbjs rewrites these methods to avoid pointing this to Window in runtime. Array.prototype.sort = (function(sort) { return function(callback) { return (this == window) ? null : (callback ? sort.call(this,function(a,b) { return callback(a,b)}) : sort.call(this)); }})(Array.prototype.sort); It even simply deletes reduce and reduceRight methods. Array.prototype.reduce = null; Array.prototype.reduceRight = null; DOM To enable third-party JavaScript code to control page elements, fbjs encapsulates many DOM methods and performs runtime checks on them. getElementById Because IDs are assigned with prefixes, the APIs provided by fbjs for the third-party JavaScript code also need to add the prefixes to get the element. 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 Checks are required for moving DOM nodes to prevent third-party JavaScript code from affecting other parts of the page. For example, to get the parentNode, it needs to be limited to within the third-party application container, not higher than other elements of this node. createElement, innerHTML Fbjs provides the createElement interface for third-party JavaScript code to create elements, and performs checks to only allow the creation of some secure elements. Many developers like to use innerHTML which is hard to perform security checks on, because HTML needs to be parsed during runtime. In view of this, fbjs only provides two methods: one is setInnerFBML, which enables FBML for parsing through a self-defined tag fb:js-string in FBML. But it has a disadvantage that it cannot be assembled during runtime. The other method is setInnerXHTML which requires a legal XML format for the passed character string and enables the XML parser of the browser for parsing. location, src, href There is a type of risk that an a tag can be dynamically created and its href can be set to generate x. So such URL attributes should be subject to judgment. 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'); } } In addition, there are also the src and location.href of img. setTimeout, setInterval The setTimeout and setInterval functions can be executed by passing character strings, but static analysis does not apply to them, too, like eval. For example: var a = 'ale'; var b = 'rt()'; setTimeout(a+b, 10); For this reason, fbjs encapsulate the two functions, only allowing them to pass function types. 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); } } Execution of JavaScript code The FBML-parsed JavaScript code cannot be put back directly. 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); } Why? One reason I can think of should be the potential loopholes from the script tags in the JavaScript characters, which impedes static analysis. For example, the code below will execute alert in IE and Chrome: <script> var a = "</script><script>alert()</script>"; </script> But FBML uses Firefox engine for parsing HTML, so the first tag in the character string has been truncated. Hidden issues It seems that the FBML solution is perfect, but it is far from that simple. There are many Facebook loopholes reported about Facebook script injection vulnerabilities, such as the E4X syntax supported by Firefox. But as Facebook is based on Firefox2 parsing engine, no issues are detected during the parsing process. <script> <x x=" x" {alert('any javascript')}="x" /> </script> In fbjs, there are so many case-by-case loophole repairs that it is impossible to predict the browser features, especially the error tolerance mechanism for HTML parsing of the browser (this is also a key problem addressed in HTML5. If interested, see the Chapter 8). But in general, the FBML solution is good and has stood numerous trials online. Microsoft Web Sandbox Microsoft Web Sandbox is a solution proposed by Microsoft. Its biggest feature is the introduction of a VM concept, transforming all code into the VM for calling and scheduling many security checks during runtime. All the HTML tags and all the JavaScript objects and DOM calls have been encapsulated so that the third-party code is entirely isolated from the external environment. Implementation Let’s see how it is implemented in detail. First, HTML source code is converted to JavaScript variables, as below: <html> <body> <a id="b" href="javascript:alert()">click</a> </body> </html> After conversion, it is changed to: var settings = {}; var headerJavaScript = function(a) { var b = a.gw(this), //Get the global objects inside the VM c = a.g, //Get the object attributes, such as the document d = a.i, //Call a method, such as the alert e = a.f, //Encapsulate the function to monitor the function running, such as to avoid endless loops and frequent alerts f = c(b,"document"); //Call initializeHTML to generate the 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(); For HTML and CSS, Tokenization is adopted to convert code into JSON format. It is worth noting that the code here is not converted as it is in FBML, but assembled during runtime. For example, the code below <a id="b" href="javascript:alert()">click</a> is converted to JSON variables. {"a": { "a":{ "href":e(function() { d(b,"alert") }), "id":"b" }, "c":[,"click"] } } It is finally assembled to html by the runtime library, as below: <a id="cst9" href="javascript:$Sandbox.Instances['__S__8'].__exechref__(0)">click</a> This facilitates migration and multiple instances. Because the generated code is like commands of the VM, instead of being bonded to a specific ID in FBML Web Sandbox can run several instances with identical third-party code at the same time by simply initiating a new $sandbox. The VM mechanism of Web Sandbox enables it to perform a number of runtime checks to solve the endless loops and infinite alerts, which is beyond the reach of other solutions, making Web Sandbox the most powerful one among the present solutions. However, only its runtime library is open-source, and its parsing is not open-source yet, so you can only manually perform parsing operations. ADsafe Unlike the above mentioned solutions, ADsafe by Douglas Crockford adopts another method - it doesn’t convert code, but verifies it. Third-party JavaScript code is only allowed for execution after it passes verifications of JSLint. Such verifications include: No global objects other than the ones provided by ADsafe are allowed, and Array Math is limited Functions of this, arguments, eval and with are forbidden Methods of arguments, callee, caller, constructor, eval, prototype, stack, unwatch, valueOf and watch are forbidden Strings starting of ending with “_” are forbidden [] is forbidden Date and Math.random functions are forbidden to eliminate randomness of widget running ADsafe doesn’t convert code, saving a lot of runtime performance, but this has led to many restrictions. Third-party developers may take some time to study the rules, such as dropping the use of “[]” and using ADSAFE.get ADSAFE.set instead. However, the biggest advantage of ADsafe lies in that it does not need to rely on the back end, thus suitable for simple widgets and embedded third-party ads. The administrator only needs to verify the third-party JavaScript code in JSLint even if the administrator knows nothing about JavaScript. In addition, disabling Date and Math.random functions makes the execution of JavaScript code stable. Below is a simple example: <div id="WIDGETNAME_"><input type=text /> </div> <script> "use strict"; ADSAFE.go("WIDGETNAME_", function (dom) { var input = dom.q("input_text"); }); </script> Why Caja If you have carefully read the introductions about the solutions above, you will know how complicated it, FBML for example, is. The dynamic nature of JavaScript language and realization of various VMs will cause many loopholes. To solve this problem, you must take every scenario into consideration, which proves to be a huge workload. So why Caja? To clarify, this is not my choice, but a solution determined by my boss and a former colleague. But as I have maintained it for such a long time, I believe it possesses the following advantages over its peers, at least for the moment: 1. Caja adopts a combination of language compiling and runtime environment, thus being more reliable. 2. Caja adopts a white list mechanism. After the runtime environment of the client API is re-written, all the methods will be forwarded to the running VM. This requires explicit declarations for all the open APIs to avoid inconsistent realization of different browsers or security issues brought up by new interfaces. 3. The interface implementation tends to be more delicate and improved, taking the innerHTML method for example. This method must be available to ISV, and must be added to our white list verification rules. In Caja, the HTML and CSS are both configured in whitelist.json and convenient to edit. 4. Sound attribute descriptors. I will not go into details about the attribute descriptors. However, there is a key point worth noting that FBML is not being maintained now so it also has too many problems to serve as a mature technical solution. ADsafe has too many restrictions and Web Sandbox is not open-source. So there are few choices left. Of course, the impeccable Caja also results in complexity. But the good news is that we have basically mastered its technology and its internal principles for applying it to our products and injecting our KISSY library for Taobao front-end framework, ensuring great convenience for our external ISVs. Status Quo and Future Trend In fact, all the above discussions are based on one premise: the client browser is not controllable and the existence of low-end browsers such as IE6, 7 and 8 leaves us less options. But new features of high-end browsers have provided a vast space for us in terms of security. For example, **Content-Security-Policy** Introduction This feature, in short, means that the server can return a response header, specifying the URLs allowed for access for the client browser. This is very cool. Because in this way, we can at least prevent data leakage, cookie theft and phishing websites resulting from the ISV-developed front-end JavaScript code. It also helps to prevent ISV-introduced external scripts from breaking through our limitation. Used in concert with some JavaScript language analysis, a majority of security issues can be solved. Mobile webview. We know that webview is actually a browser, it is only customized by us. We can impose monitoring and limits on the requests or navigations in the webview, which is similar to the content-security-policy mentioned above. If some interfaces enable us to have certain security policy control powers on the browser client, our solution can be reconsidered. Caja may be too tedious, as some of the security issues it focuses on are not within our consideration, so it needs to be trimmed in future. Summary At the beginning of this article, the author introduced some business background and then briefed the JavaScript security policy and solutions, and summarized the status quo and future trend in the last section. For the time being, it is hard to manage the buyer-end on PC because of low-end browsers, and Caja is indeed a good solution. We can designate the client browser, and adopt some content security policies or browser plug-ins to achieve “post monitoring policy” For mobile terminals, we can achieve security control through customizing our webview. |
|