Is it possible to sandbox JavaScript running in the browser?
Solution 1:
Google Caja is a source-to-source translator that "allows you to put untrusted third-party HTML and JavaScript inline in your page and still be secure."
Solution 2:
Have a look at Douglas Crockford's ADsafe:
ADsafe makes it safe to put guest code (such as third party scripted advertising or widgets) on any web page. ADsafe defines a subset of JavaScript that is powerful enough to allow guest code to perform valuable interactions, while at the same time preventing malicious or accidental damage or intrusion. The ADsafe subset can be verified mechanically by tools like JSLint so that no human inspection is necessary to review guest code for safety. The ADsafe subset also enforces good coding practices, increasing the likelihood that guest code will run correctly.
You can see an example of how to use ADsafe by looking at the template.html
and template.js
files in the project's GitHub repository.
Solution 3:
I created a sandboxing library called jsandbox that uses web workers to sandbox evaluated code. It also has an input method for explicitly giving sandboxed code data it wouldn't otherwise be able to get.
The following is an example of the API:
jsandbox
.eval({
code : "x=1;Math.round(Math.pow(input, ++x))",
input : 36.565010597564445,
callback: function(n) {
console.log("number: ", n); // number: 1337
}
}).eval({
code : "][];.]\\ (*# ($(! ~",
onerror: function(ex) {
console.log("syntax error: ", ex); // syntax error: [error object]
}
}).eval({
code : '"foo"+input',
input : "bar",
callback: function(str) {
console.log("string: ", str); // string: foobar
}
}).eval({
code : "({q:1, w:2})",
callback: function(obj) {
console.log("object: ", obj); // object: object q=1 w=2
}
}).eval({
code : "[1, 2, 3].concat(input)",
input : [4, 5, 6],
callback: function(arr) {
console.log("array: ", arr); // array: [1, 2, 3, 4, 5, 6]
}
}).eval({
code : "function x(z){this.y=z;};new x(input)",
input : 4,
callback: function(x) {
console.log("new x: ", x); // new x: object y=4
}
});
Solution 4:
As mentioned in other responces, it's enough to jail the code in a sandboxed iframe (without sending it to the server-side) and communicate with messages.
I would suggest to take a look at a small library I created mostly because of the need to providing some API to the untrusted code, just like as described in the question: there's an opportunity to export the particular set of functions right into the sandbox where the untrusted code runs. And there's also a demo which executes the code submitted by a user in a sandbox:
http://asvd.github.io/jailed/demos/web/console/
Solution 5:
An improved version of RyanOHara's web workers sandbox code, in a single file (no extra eval.js
file is necessary).
function safeEval(untrustedCode)
{
return new Promise(function (resolve, reject)
{
var blobURL = URL.createObjectURL(new Blob([
"(",
function ()
{
var _postMessage = postMessage;
var _addEventListener = addEventListener;
(function (obj)
{
"use strict";
var current = obj;
var keepProperties =
[
// Required
'Object', 'Function', 'Infinity', 'NaN', 'undefined', 'caches', 'TEMPORARY', 'PERSISTENT',
// Optional, but trivial to get back
'Array', 'Boolean', 'Number', 'String', 'Symbol',
// Optional
'Map', 'Math', 'Set',
];
do
{
Object.getOwnPropertyNames(current).forEach(function (name)
{
if (keepProperties.indexOf(name) === -1)
{
delete current[name];
}
});
current = Object.getPrototypeOf(current);
}
while (current !== Object.prototype)
;
})(this);
_addEventListener("message", function (e)
{
var f = new Function("", "return (" + e.data + "\n);");
_postMessage(f());
});
}.toString(),
")()"],
{type: "application/javascript"}));
var worker = new Worker(blobURL);
URL.revokeObjectURL(blobURL);
worker.onmessage = function (evt)
{
worker.terminate();
resolve(evt.data);
};
worker.onerror = function (evt)
{
reject(new Error(evt.message));
};
worker.postMessage(untrustedCode);
setTimeout(function ()
{
worker.terminate();
reject(new Error('The worker timed out.'));
}, 1000);
});
}
Test it:
https://jsfiddle.net/kp0cq6yw/
var promise = safeEval("1+2+3");
promise.then(function (result) {
alert(result);
});
It should output 6
(tested in Chrome and Firefox).