My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Sunday, July 12, 2009

Elsewhere - Sandboxes Have Never Been That Easy

One of the most common problems in Web 2.0 Development, is the amount of libraries we can possibly include into a single project and conflicts these libraries could have.
Some library is truly aggressive. It could change the entire behavior of each other lib present in the page via not-standard or future implementations of some global object prototype, such String, Object itseld, Array, or others.
Since this is a well known problem, some library/framework can try to avoid conflicts in different ways. As example, jQuery has a noConflict method which aim is to make development as painless as possible ... but it is not always that simple. One of Elsewhere purpose is to connect a completely different and external scope into the main window, avoiding any sort of conflict a library with its global properties, functions, or prototypes, could have. First example:

new Elsewhere(
"scripts/libs/jquery.1.3.2.min.js"
).execute(function(ready){
// this scope is elsewhere in a perfectly clean environment.
// In this case jQuery will be manifested as global "$" function.
// To bring jQuery outside, we can use this sandbox parent reference
parent.jQuery = function(search, context){
// to make sure we do not search in an "empty" document,
// the one part of this sandbox, we pass the context
// if not present as parent.document
return $(search, context || parent.document);
};

// we can call the ready callback
ready(parent.jQuery);
},
[ // one or more arguments to pass into Elsewhere function execution
function($){

// let's do something on ready event
// please note there is no var in front of jQuery
// since it has been declared as global
jQuery(function(){
// we could use the argument as well
// since in this case it is exactly
// the same jQuery function
$("body").html("Here I Am");
});

}
]);

Confused? Please read more ;)

Quick Answer: What Is A Sandbox


Every time we open a new window in our browser, the latter one creates a (almost)clean environment to run, if presents, each JavaScript file. The same happens for each tab, which means that if we define a function Z(){} in tab a, this will not be present in tab b. Finally, same things happens inside frames or iframes as well. Each window, framed, tabbed, or not, will have then its own JavaScript environment and in JS world this is called sandbox.

What Is Elsewhere


Elsewhere is a lightweight, less than 1Kb if minified and gzipped, almost fully cross-browser library (so far compatible with each browser I tested) able to create runtime a sandbox including zero, one, or more scripts. The first showed example, creates a sandbox including jQuery library and bringing its power outside the box.
The important thing to understand, is that used function scope will be completely extraneous to the one the function itself has been created ... in few words, that scope will be elsewhere!
Thanks to possibility to send arguments when this external function will be executed, and thanks to parent or top reference, it is possible to play between different scopes avoiding conflicts, creating bridges, loading JSONP, and more!

// JSONP example
new Elsewhere(
"http://mycoolservice.com?fn=parent.callback"
);
// that's it!

About JSONP, via Elsewhere a response could be more than just a callback. It could be an entire different environment with runtime created bridges. Example:

// output produced by a generic REST service
// myservice.server/name/WebReflection

// a function which name is not a problem
function getReferences(){
return userInfo.name + " has " + userInfo.posts + " posts";
};

// a global object which name is not a problem
var userInfo = {
name:"WebReflection",
site:"http://webreflection.blogspot.com/",
posts:12345
};

// a bridge for Elsewhere instance
// every Elsewhere sandbox has a reference to its isntance
// This reference is retrieved via global sandbox window object
// and the property @_Elsewhere
window["@_Elsewhere"].getUserProperty = function(name){
return userInfo[name];
};

// please note you cannot access that property directly
// window.@_Elsewhere will cause an error
window["@_Elsewhere"].getReferences = getReferences;

// ...

// our main page via Elsewhere
var myservice = new Elsewhere("myservice.server/name/WebReflection");
myservice.execute(function(fn){fn()}, [function(){
alert(myservice.getUserProperty("site")); // http://webreflection.blogspot.com/
alert(myservice.getReferences()); // WebReflection has 12345 posts
}]);


Understanding execute Method: How Elsewhere Works


How can be possible that a function defined inside a scope is totally ignorant about surrounding variables or scopes? It is really simple!
Elsewhere is based on Function.prototype.toString de-compilation, creating passed function directly as script inside the sandbox and returning, if status is ready, its value or undefined.
The important thing to understand is that if we create a global scope variable, this will be present next call, whenever we decide to perform it.

var sb = new Elsewhere();
// sb.ready is true, no external scripts loaded
alert(sb.execute(function(){
// generic function
function sum(a, b){
return a + b;
};

// define sum as global in this scope
window.sum = sum;

// test the function
return sum(1, 2);

})); // will be 3


// do other stuff and ...
sb.execute(function(){
return sum(3, 4);
}); // will be 7, sum was already defined

alert(typeof sum); // undefined
// sum exists only Elsewhere

About the ready status flag, it is instantly true if we do not load external script, while it could be true or false, depending on which browser we are using (in some browser an iframe document with external scripts is loaded sync while with Firefox 3.5 an iframe does not block main page content so it is not sync).

// more than a file ...
new Elsewhere([
// order is respected and reliable
"jquery.js",
"jquery.ui.js"
]).execute(function(onjQueryAndUILoaded, initialTime){
// execute is always performed
// after everything has been loaded
onjQueryAndUILoaded(jQuery, new Date - initialTime);
}, [
// we can optionally pass one or more arguments
function($, elaspedTime){
// let's understand how much it took
alert(elaspedTime);
},
new Date
]);


Understanding extend Method: Fast Bridge


Every time we execute a function inside an Elsewhere instance, we are de-compiling and injecting it inside another document. This operation is not that expensive but if we need to perform some task in that scope frequently, we could consider to extend an Elsewhere instance via its own method, creating a sort of fast bridge between two or more than two different worlds.

// empty sandbox
var sb = new Elsewhere()
// extend returns the instance itself
.extend(
{
extendObject:function(){
var toString = Object.prototype.toString;

// note, this Object is not the main one
// but the one from this sandbox (main not affected)
Object.prototype.type = function(){
var s = toString.call(this);
return s.substr(8, s.length - 9);
};

// avoid future modification
window.extendObject = function(){};
},
createObject:function(o){
// create an Elsewhere Object
return new Object(o);
},

// if an extend key is a global variable
// different from undefined
// this will simply be the variable from
// this sandbox
Object:1,

// if the value is not a function
// it will simply be saved as is
cool:true
}
);

// extend Elsewhere Object.prototype
sb.extendObject();

// test it
var o = sb.createObject("test");
alert(o.type()); // String

// o is not instanceof this main page Object
alert(o instanceof Object); // false

// but it could be used as if was
// an Object
alert(o instanceof sb.Object); // true

alert(sb.cool); // true


As Summary


Elsewhere can open hundreds of possibilities without bringing nothing new, except that browsers are starting to implement different threads for each sandbox (tab, or window) and I expect this library will bring an even more simple way to implement web workers. Right now main points are conflicts resolution, when libraries are able to deal from a sandbox, new JSONP interaction ways, multiple native constructor prototype definitions via one or more Elsewhere instances, just a simple way to load external scripts or ... well, whatever else our imagination could create with such tiny, cross-browser, library. Have fun with sandboxes 8).

The script? Via post title or here, in devpro.it.

14 comments:

Josh Powell said...

Did you create this? It's brilliant.

Andrea Giammarchi said...

Josh, of course I created it ;) Cheers

rick said...

Very cool.

Anonymous said...

look like my experiment http://github.com/Steida/PJ/tree/master

Unknown said...

Hi!

I've been playing with this for some time in the past, and I think you should express that this sandbox should not be used for security, since sandboxed scripts have access to the parent window (as you show).

I have made a safe sandbox using this approach that only works on firefox (safe meaning that it wont allow dangerous code to be execute nor traverse the DOM).

//sandbox.sirdarckcat.net/

I also have a contest that uses the sandbox! if you are interested :)

Oh, and jQuery wont work in all cases (but it works most of the time anyway) because you can't import nodes in the current document if they created on other documents (eg. HTMLElement.ownerDocument is different) and are cloned nodes (in that case you should use the importNode function, thing that jQuery doesnt do). This is a non issue most of the times since the operations that jQuery does don't make reference to the document untill they are appended, but a code like this one:

parent.jQuery=$;
parent.jQuery("\x3cimg src=x onerror=alert(location)>")

will alert the framed location (just to give an example, in WebKit and IE there are some other scenarios)

Greetings!!

Andrea Giammarchi said...

Daniel, Elsewhere purpose is to create one or more sandboxes and execute whatever you need in a clean environment. Elsewhere itself does not change anything about that sandbox, so it is a bit more flexible than your PJ library which is interesting in any case and I did not know it.

Andrea Giammarchi said...

sdc, there is nothing secure in any case and Elsewhere purpose as I said is to create an empty environment. Elsewhere purpose is NOT to create anything more secure because security cannot be obtained via a sandbox while a scope with global function or prototypes will not affect and will not be affected from other scripts. That is why I have already implemented a sandbox version of jQuery which could run without problems in a window with Prototype or other libraries.

have a look ;)

Unknown said...

> security cannot be obtained via a sandbox
I beg to differ, at least on firefox this is possible, check he previous link

Andrea Giammarchi said...

sdc, the document issue has been solved today via my patch to jQuery, check that post/ticket so I am able to use 100% of jQuery from a sandbox without problems (a simple search via id is a problem if document is not the correct one).

About safety, again, to retrieve the native sandbox window you just need this:
var window = function(){return this}();
you can use delete to retrieve original objects (not exactly original but a new copy of them).

I have never said Elsewhere make JavaScript more secure, all I was talking about is compatibility between libraries and a space to put whatever we want.

I do not think a sandbox can be considered safe, but if you think so, it is just a matter of time before you'll think different, IMHO :)

Unknown said...

So, the problem here is the use of the term "sandbox".

http://en.wikipedia.org/wiki/Sandbox_(computer_security)

vs.

http://en.wikipedia.org/wiki/Sandbox_(software_development)

As I understand sandbox is something that is meant to be to run unsafe code. I didnt know there was another definition of sandbox.

Oh! and about the sandbox escaping tricks, I know about them, actually I'm very familiar with them.. I could say its my job haha, but as I said before a couple of times, a safe sandbox in firefox is possible.. (acknowledged by gareth heyes, giorgio maone, mario heiderich, and a few other pros :D)

Greetings!!

Andrea Giammarchi said...

The therm is correct, JavaScript speaking. Gareth asked me to test its sandbox since the beginning and I still tell you it is not safe. Feel free to think what you want but please stop with this flame, OK? Elsewhere is not about security it is about sandboxing, which is a completely different thing, got it? :)

Unknown said...

very well :P, just to let you know, in that last post I told you you were right (note this part: "I didnt know there was another definition of sandbox.").

Andrea Giammarchi said...

uh, ok sorry, I silly stopped at the first link. Apologies

Markus said...

Hope you still monitor this. Do you have a reference which browsers block when a script tag is loaded in an iframe? I currently try to find a way for proper error handling with jsonp when one request goes to /dev/null and is blocking the execution of the rest.