Basket
Basket is a simple method for creating a new JavaScript context in the browser, e.g. for loading external scripts into a page without affecting the global scope of the document. This is useful when there is a need to avoid collisions between otherwise conflicting scripts and to keep the global namespace clean - e.g. when including third-party JavaScript widgets in a web page (see the Sqwidget project, which started the original development in the form of Sandie).
Scripts are sandboxed by loading them inside the window of a temporary iframe, and then returned to a callback function, so that they can be used elsewhere on the main document.
Who to blame
- Based on Sandie by Premasagar Rose.
- Continued development by samrland.
- Served for the people under the MIT License.
How to use
Just use an HTML script tag:
<script src="https://basketjs.netlify.app/basket.js" />
What to do
Take a look at the demo here.
Use cases
- Allowing widgets to load different versions of the same plugin or JavaScript library
- Allowing widgets to load different scripts that use the same names for global variables
- Keeping widgets away from modifying local user data (see "Security" section)
Avoid var collisions between competing scripts
var foo = 'bar';
basket(
'script.js', // this example script contains "var foo = 'blah';"
function(exports) { // a callback function, containing vars added by the script
alert(foo); // 'bar' (from global scope in the main document)
alert(exports.foo); // 'blah' (from the sandboxed script)
}
);
Load multiple scripts
basket(
[ 'script1.js', 'script2.js' ], // multiple script src's
function(exports) { // 'exports' is an object, with properties 'foo1' & 'foo2' from the scripts
alert(exports.foo1 + exports.foo2);
}
);
Load objects and pass functions into the sandbox
basket(
[ { foo1: bar }, function() { this.foo2 = 'blah'; }, 'foo3.js' ], // objects, functions and external scripts
function(exports) { // 'exports' is a key-value object of 'foo1', 'foo2' & 'foo3'
alert(exports.foo1 + exports.foo2 + exports.foo3);
}
);
In this example, an object is merged into the global scope of the sandbox (i.e. the iframe window
), a function is executed in the scope of the sandbox, and then an external script is loaded.
This is useful, for example, when certain variables and functions are required to be in place before loading an external script.
Eval code in the sandbox
Pass a string of code, contained within a <script></script>
wrapper:
basket('<script>var evilThing = "foo";</script>', callback);
Scripts to be eval'ed can also be passed along with the other types of scripts (external scripts, objects and functions), but (for the moment, at least), the eval'ed script should come first:
basket(['<script>var evilThing = "foo";</script>', { foo: 'bar' }, 'example.js'], callback);
This can be utilised, for example, to prevent a malicious script from accessing global properties, by masking the window
property:
basket(['<script>var window = {};</script>', 'evil.js'], callback);
Or by masking more fine-grained properties:
basket(['<script>window.parent = null;</script>', 'evil.js'], callback);
See the note on "Security", below.
persist
Optional third argument: By default, the sandbox will clean up after itself once all the scripts have loaded. To keep keep the sandbox open, so that further scripts can be loaded or further manipulations can be made, simply pass boolean true
as the third argument.
The callback function can now be used to manipulate the sandbox further, e.g. via the load()
method, which takes the same arguments as the initial constructor:
basket(
'example.js',
function() {
this.load('example2.js', function(vars) {
doSomething(vars);
});
},
true
);
Document additional methods
Until documentation is complete, check out the source code of basket.js
to see what's available.
Security
The initial use case for Sandie was to contain potentially messy, but otherwise trusted, third-party scripts. However, it is also possible to contain malicious scripts. E.g. you can prevent a malicious script from accessing global properties, by masking the local window
variable:
basket(['<script>var window = {};</script>', 'evil.js'], callback);
In this example, the malicious script will be unable to access the global parent window or the local document. (Disclaimer: this has not yet been rigorously tested; please do share any research you perform in this area).
Some third-party might legitimately want to access the containing window. Such scripts will break when the sandbox is locked down in this way.
A more sophisticated API for bulletproofing a secure sandbox will be added to Sandie in future. (Your patches and pull requests are very welcome).