dojo.provide("dojox.secure.sandbox"); dojo.require("dojox.secure.DOM"); dojo.require("dojox.secure.capability"); dojo.require("dojo.NodeList-fx"); (function() { var oldTimeout = setTimeout; var oldInterval = setInterval; if({}.__proto__){ // mozilla has unsafe methods on array var fixMozArrayFunction = function (name) { var method = Array.prototype[name]; if(method && !method.fixed){ (Array.prototype[name] = function () { if (this == window) { throw new TypeError("Called with wrong this"); } return method.apply(this, arguments); }).fixed = true; } }; // these are not safe in mozilla fixMozArrayFunction('concat'); fixMozArrayFunction('reverse'); fixMozArrayFunction('sort'); fixMozArrayFunction("slice"); fixMozArrayFunction("forEach"); fixMozArrayFunction("filter"); fixMozArrayFunction("reduce"); fixMozArrayFunction("reduceRight"); fixMozArrayFunction("every"); fixMozArrayFunction("map"); fixMozArrayFunction("some"); } var xhrGet = function(){ return dojo.xhrGet.apply(dojo,arguments); }; dojox.secure.sandbox = function(element) { // summary: // Creates a secure sandbox from which scripts and HTML can be loaded that // will only be able to access the provided element and it's descendants, the // rest of the DOM and JS environment will not be accessible to the sandboxed // scripts and HTML. // // element: // The DOM element to use as the container for the sandbox // // description: // This function will create and return a sandbox object (see dojox.secure.__Sandbox) // for the provided element. var wrap = dojox.secure.DOM(element); element = wrap(element); var document = element.ownerDocument; var mixin, dojo = dojox.secure._safeDojoFunctions(element,wrap); var imports= []; var safeCalls = ["isNaN","isFinite","parseInt","parseFloat","escape","unescape", "encodeURI","encodeURIComponent","decodeURI","decodeURIComponent", "alert","confirm","prompt", // some people may not want to allow these to be called, but they don't break capability-limiting "Error","EvalError","RangeError","ReferenceError","SyntaxError","TypeError", "Date","RegExp","Number","Object","Array","String","Math", //"ADSAFE", // not using ADSAFE runtime for the time being "setTimeout","setInterval","clearTimeout","clearInterval", // we make these safe below "dojo","get","set","forEach","load","evaluate"]; for(var i in dojo){ safeCalls.push(i); // add the safe dojo functions to as available global top level functions imports.push("var " + i + "=dojo." + i); // add to the list of imports } // open the dojo namespace (namespaces are pretty silly in an environment where you can't set globals) eval(imports.join(";")); function get(obj,prop) { // basic access by index function prop = '' + prop; if(dojox.secure.badProps.test(prop)) { throw new Error("bad property access"); } if(obj.__get__) { return obj.__get__(prop); } return obj[prop]; } function set(obj,prop,value) { // basic set by index function prop = '' + prop; get(obj,prop); // test it if(obj.__set) { return obj.__set(prop); } obj[prop] = value; return value; } function forEach(obj,fun) { // short syntax iterator function if(typeof fun != "function"){ throw new TypeError(); } if("length" in obj) { // do arrays the fast way if(obj.__get__) { // use the catch getter var len = obj.__get__('length'); for (var i = 0; i < len; i++) { if(i in obj) { fun.call(obj, obj.__get__(i), i, obj); } } } else { // fast len = obj.length; for (i = 0; i < len; i++) { if(i in obj) { fun.call(obj, obj[i], i, obj); } } } } else { // for each an object for (i in obj) { fun.call(obj, get(obj,i), i, obj); } } } function Class(/*Function*/superclass, /*Object*/properties, /*Object*/classProperties) { // summary: // A safe class constructor // // superclass: // There may be zero or more superclass arguments. The constructed class // will inherit from any provided superclasses, protypically from the first, // via mixin for the subsequent. Later arguments // will override properties/methods from earlier arguments // // properties: // The constructed // "class" will also have the methods/properties defined in this argument. // These methods may utilize the this operator, and they // are only the code that has access to this. Inner functions // are also prohibited from using this. // // If no superclasses are provided, this object will be the prototype of the // constructed class (no copying // will be done). Consequently you can "beget" by calling new (Class(obj)). // All methods are "bound", each call results in |this| safety checking call. // // classProperties: // This properties will be copied to the new class function. // // Note that neither dojo.declare nor dojo.extend are acceptable class constructors as // they are completely unsecure. This class constructor is conceptually based on declare // but also somewhat influenced by base2, prototype, YUI, resig's patterns, etc. // // example: // | var Car = Class({drive:function(speed) { ... } ); // create a Car class with a "drive" method // | var FastCar = Class(Car,{driveFast: function(speed) { return this.drive(2 * speed); } }); // create a FastCar that extends Car // | var fastCar = new FastCar; // instantiate // | fastCar.driveFast(50); // call a method // | var driveFast = fastCar.driveFast; // | var driveFast(50); // this will throw an error, the method can be used with an object that is not an instance of FastCar var proto,superConstructor,ourConstructor; var arg; for (var i = 0, l = arguments.length; typeof (arg = arguments[i]) == 'function' && i < l; i++) { // go through each superclass argument if(proto) { // we have a prototype now, we must mixin now mixin(proto,arg.prototype); } else { // this is the first argument, so we can define the prototype ourselves // link up the prototype chain to the superclass's prototype, so we are a subtype superConstructor = arg; var F = function() {}; F.prototype = arg.prototype; proto = new F; } } if(arg) { // the next object should be the properties // apply binding checking on all the functions for (var j in arg) { // TODO: check on non-enumerables? var value = arg[j]; if(typeof value == 'function') { arg[j] = function() { if(this instanceof Class){ return arguments.callee.__rawMethod__.apply(this,arguments); } throw new Error("Method called on wrong object"); }; arg[j].__rawMethod__ = value; // may want to use this for reconstruction and toString,valueOf } } if(arg.hasOwnProperty('constructor')) { ourConstructor = arg.constructor; } } proto = proto ? mixin(proto,arg) : arg; // if there is no proto yet, we can use the provided object function Class() { // the super class may not have been constructed using the same technique, we will just call the constructor if(superConstructor){ superConstructor.apply(this,arguments); } if(ourConstructor){ ourConstructor.apply(this,arguments); } } mixin(Class,arguments[i]); // the optional second object adds properties to the class proto.constructor = Class; Class.prototype = proto; return Class; } function checkString(func){ if(typeof func != 'function') { throw new Error("String is not allowed in setTimeout/setInterval"); } } function setTimeout(func,time) { // sandboxed setTimeout checkString(func); return oldTimeout(func,time); } function setInterval(func,time) { // sandboxed setInterval checkString(func); return oldInterval(func,time); } function evaluate(script){ // sandboxed eval return wrap.evaluate(script); } var load = wrap.load = function(url){ // provides a loader function for the sandbox if (url.match(/^[\w\s]*:/)){ throw new Error("Access denied to cross-site requests"); } return xhrGet({url:(new dojo._Url(wrap.rootUrl,url))+'',secure:true}); } wrap.evaluate = function(script){ //if(!alreadyValidated) { dojox.secure.capability.validate(script,safeCalls, // the safe dojo library and standard operators {document:1,element:1}); // these are secured DOM starting points //} if(script.match(/^\s*[\[\{]/)) { var result = eval('(' + script + ')'); // TODO: call render on result? } else { eval(script); } //eval('wrap.evaluate=('+arguments.callee.toString()+')'); // yeah, recursive scoping; }; return /*===== dojo.declare("dojox.secure.__Sandbox", null, =====*/ { // dojox.secure.__Sandbox loadJS : function(url){ // summary: // Loads the script from the given URL using XHR (assuming // a plugin system is in place for cross-site requests) within the sandbox // // url: // The url of the script to load wrap.rootUrl = url; return xhrGet({url:url,secure:true}).addCallback(function(result) { evaluate(result,element /*If we get the results with a secure proxy, we would call put true here */); }); }, loadHTML : function(url){ // summary: // Loads the web page from the provided URL using XHR (assuming the // plugin system is in place) within the sandbox. All scripts within the web // page will also be sandboxed. // // url: // The url of the web page to load wrap.rootUrl = url; return xhrGet({url:url,secure:true}).addCallback(function(result){ element.innerHTML = result; }); }, evaluate : function(script){ // summary: // Evaluates the given script within the sandbox // // script: // The JavaScript text to evaluate return wrap.evaluate(script); } // TODO: could add something for pre-validated scripts }/*===== ) =====*/; }; })(); dojox.secure._safeDojoFunctions = function(element,wrap) { // Creates a safe subset of Dojo core library var safeFunctions = ["mixin","require","isString","isArray","isFunction","isObject","isArrayLike","isAlien", "hitch","delegate","partial","trim","disconnect","subscribe","unsubscribe","Deferred","toJson","style","attr"]; //var domFunctions = ["clone","byId"]; var doc = element.ownerDocument; var unwrap = dojox.secure.unwrap; dojo.NodeList.prototype.addContent.safetyCheck = function(content){ wrap.safeHTML(content); }; dojo.NodeList.prototype.style.safetyCheck = function(name,value){ if(name=='behavior'){ throw new Error("Can not set behavior"); } wrap.safeCSS(value); }; dojo.NodeList.prototype.attr.safetyCheck = function(name,value){ if (value && (name == 'src' || name == 'href' || name=='style')){ throw new Error("Illegal to set " + name); } }; var safe = { query : function(query,root) { return wrap(dojo.query(query,unwrap(root || element))); // wrap the NodeList }, connect: function(el,event) { var obj = el; arguments[0] = unwrap(el); if(obj!=arguments[0] && event.substring(0,2) != 'on'){ // it is probably an element, and it doesn't look like an event handler, probably not safe throw new Error("Invalid event name for element"); } return dojo.connect.apply(dojo,arguments); }, body : function() { return element; }, byId : function(id) { return element.ownerDocument.getElementById(id); // use the safe document }, fromJson : function(str) { // make sure it is safe before passing it to the unsafe dojo.fromJson dojox.secure.capability.validate(str,[],{}); return dojo.fromJson(str); } }; for (var i = 0; i < safeFunctions.length; i++) { safe[safeFunctions[i]] = dojo[safeFunctions[i]]; } return safe; };