170

I'm wondering if it's possible to sandbox JavaScript running in the browser to prevent access to features that are normally available to JavaScript code running in an HTML page.

For example, let's say I want to provide a JavaScript API for end users to let them define event handlers to be run when "interesting events" happen, but I don't want those users to access the properties and functions of the window object. Am I able to do this?

In the simplest case, let's say I want to prevent users calling alert. A couple of approaches I can think of are:

  • Redefine window.alert globally. I don't think this would be a valid approach because other code running in the page (i.e., stuff not authored by users in their event handlers) might want to use alert.
  • Send the event handler code to the server to process. I'm not sure that sending the code to the server to process is the right approach, because the event handlers need to run in the context of the page.

Perhaps a solution where the server processes the user defined function and then generates a callback to be executed on the client would work? Even if that approach works, are there better ways to solve this problem?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Walter Rumsby
  • 7,435
  • 5
  • 41
  • 36

15 Answers15

55

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."

MattCochrane
  • 2,900
  • 2
  • 25
  • 35
Darius Bacon
  • 14,921
  • 5
  • 53
  • 53
  • 6
    A quick test shows that Caja is unable to protect the browser from CPU attacks like `while (1) {}` --- it just hangs. Likewise `a=[]; while (1) { a=[a,a]; }`. – David Given Apr 03 '14 at 22:45
  • 6
    Yes, denial of service is out of scope: https://code.google.com/p/google-caja/issues/detail?id=1406 – Darius Bacon Apr 04 '14 at 19:53
  • 5
    Project will be deprecated by Google January 31st 2021. They're recommending people move to Closure library (https://github.com/google/closure-library) instead. – Chris Owens Dec 13 '20 at 06:37
33

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.

Simon Lieschke
  • 13,058
  • 6
  • 46
  • 60
  • 1
    On their site, I see no way of using ADsafe. There is no way to download it, no link to the code, nothing. How can you try out ADsafe? – B T Dec 10 '15 at 19:39
  • 2
    Also, it prevents *any* access to `this`, which is entirely unacceptable. You can't write good javascript without using `this`. – B T Dec 10 '15 at 19:41
  • There's a link to [the code on GitHub](https://github.com/douglascrockford/ADsafe) from the bottom of the [ADsafe Widget Structure](http://www.adsafe.org/widget.html) page. – Simon Lieschke Jan 08 '16 at 01:09
  • 11
    @BT I've written entire projects without using `this`. It's not hard to avoid the poorly-named parameter. – soundly_typed Jan 04 '17 at 02:10
  • @mindeavor Then you've entirely foregone all object-oriented javascript. I would say that's unacceptable. – B T Jan 04 '17 at 22:16
  • 6
    @BT It'd be silly to say that completing real-world projects is unacceptable. But I regret starting this discussion, and must withdraw; this is not the place to discuss such things (sorry). I'm on twitter if you want to discuss further. – soundly_typed Jan 04 '17 at 23:12
  • @mindeavor Real world projects use object-oriented programming and `this`. Please do withdraw, your arugment is garbage. Not allowing `this` means valid javascript fails. Its a recipe for being a pain in the ass. Yes you *could* do it, but by definition guest code isn't *you*, its someone else - the guest. And telling them they can't use any library that uses `this` means they're going to give you a big middle finger and never integrate with your crap. – B T Jan 05 '17 at 01:11
  • 3
    @BT (I'll continue since it's relevant to the question) Anytime you run code in someone else's environment you will run into rules and restrictions. I would not call that unacceptable. A "pain in the ass", maybe. But not unacceptable. After all, for every use of `this`, there is an equal, equivalent non-`this` way to do it (it's just a parameter, after all). – soundly_typed Jan 05 '17 at 05:00
  • how to use this? i dont see any documentation at all – slier Jan 16 '17 at 02:49
  • @slier I've edited my answer with detail of where you can see an example of using ADsafe. – Simon Lieschke Jan 16 '17 at 20:54
23

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
      }
  });
Eli Grey
  • 35,104
  • 14
  • 75
  • 93
  • +1: This looks really cool. How safe is to execute user's code this way? – Konstantin Tarkus Nov 09 '10 at 22:11
  • 1
    Very safe. Check out the updated library [on github](https://github.com/eligrey/jsandbox). – Eli Grey Nov 10 '10 at 20:55
  • 1
    is this project still maintained? I see it has not been updated since over 2 years... – Yanick Rochon Oct 29 '12 at 01:12
  • I like this, except that if you want to sandbox but still allow the code access to say jQuery, this would fail as web workers do not allow DOM manipulation. – Rahly Mar 18 '15 at 21:55
  • 2
    Hi Eli - thanks for a great lib, are you planning to maintain it? I've a change request for adding debugging functionality - which by quick looking at the code should be possible. Please let me know what you think? – user1514042 May 15 '15 at 14:47
  • 1
    @Rahly: Anything that is allowed to manipulate the DOM is, by definition, unsafe. How do you imagine you would sandbox DOM access? – Sasha Chedygov Jun 15 '20 at 19:21
  • 2
    In theory? You would create a document fragment that is jailed, where any manipulations/traversals are a limited by the fragment tree. – Rahly Jun 16 '20 at 16:29
14

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).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Marcelo Glasberg
  • 29,013
  • 23
  • 109
  • 133
12

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/

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
asvd
  • 944
  • 10
  • 16
9

I think that js.js is worth mentioning here. It's a JavaScript interpreter written in JavaScript.

It's about 200 times slower than native JavaScript, but its nature makes it a perfect sandbox environment. Another drawback is its size – almost 600 KB, which may be acceptable for desktops in some cases, but not for mobile devices.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
gronostaj
  • 2,231
  • 2
  • 23
  • 43
6

All the browser vendors and the HTML5 specification are working towards an actual sandbox property to allow sandboxed iframes -- but it's still limited to iframe granularity.

In general, no degree of regular expressions, etc. can safely sanitise arbitrary user provided JavaScript as it degenerates to the halting problem :-/

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
olliej
  • 35,755
  • 9
  • 58
  • 55
  • 2
    Can you explain how it degenerates to the halting problem? – hdgarrood Apr 24 '13 at 00:03
  • 3
    The theoretic impossibility of solving the halting problem only really applies to static code analysis. Sandboxes can do things like enforce time limits to deal with the halting problem. – Aviendha Nov 29 '15 at 19:44
2

An ugly way, but maybe this works for you:

I took all the globals and redefined them in the sandbox scope, as well I added the strict mode so they can't get the global object using an anonymous function.

function construct(constructor, args) {
  function F() {
      return constructor.apply(this, args);
  }
  F.prototype = constructor.prototype;
  return new F();
}
// Sanboxer
function sandboxcode(string, inject) {
  "use strict";
  var globals = [];
  for (var i in window) {
    // <--REMOVE THIS CONDITION
    if (i != "console")
    // REMOVE THIS CONDITION -->
    globals.push(i);
  }
  globals.push('"use strict";\n'+string);
  return construct(Function, globals).apply(inject ? inject : {});
}
sandboxcode('console.log( this, window, top , self, parent, this["jQuery"], (function(){return this;}()));');
// => Object {} undefined undefined undefined undefined undefined undefined
console.log("return of this", sandboxcode('return this;', {window:"sanboxed code"}));
// => Object {window: "sanboxed code"}

https://gist.github.com/alejandrolechuga/9381781

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
alejandro
  • 2,799
  • 1
  • 17
  • 25
  • 4
    Trivial to get `window` back from that. `sandboxcode('console.log((0,eval)("this"))')` – Ry- Jun 03 '15 at 19:44
  • I'll have to figure out how to prevent that – alejandro Jul 05 '15 at 05:17
  • @alejandro Did you find a way to prevent that? – Wilt Dec 14 '15 at 13:07
  • Broke my head until I just realized you can do ```eval = 0``` globally before calling the sandbox (storing the original function in a temp) and then both global ```window.eval``` and ```eval``` wont be accessible. Next Hack please! Because I actually consider this option. – YoniXw Dec 28 '18 at 00:33
  • 1
    My implementation just adds: ```function sbx(s,p) {e = eval; eval = function(t){console.log("GOT GOOD")}; sandboxcode(s,p); eval =e}``` – YoniXw Dec 28 '18 at 00:52
  • 4
    @YoniXw: I hope you didn’t end up using it for anything. No approach like this will ever work. `(_=>_).constructor('return this')()` – Ry- Mar 10 '19 at 03:42
  • There's no nice way to do it you can always do this and you'll have access, with an IIFE `function sandboxcode(string, inject) { "use strict"; return (new Function(\`const window = {}; var_this = this; const console = this.console; ${string}\`)).call({console}); } sandboxcode('console.log(function(){ return this}())');` – alejandro Mar 11 '19 at 00:13
2

An independent JavaScript interpreter is more likely to yield a robust sandbox than a caged version of the built-in browser implementation.

Ryan has already mentioned js.js, but a more up-to-date project is JS-Interpreter. The documentation covers how to expose various functions to the interpreter, but its scope is otherwise very limited.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
David Fraser
  • 6,475
  • 1
  • 40
  • 56
0

As of 2019, vm2 looks like the most popular and most regularly-updated solution to running JavaScript in Node.js. I'm not aware of a front-end solution.

Bret Cameron
  • 451
  • 1
  • 4
  • 18
  • 2
    vm2 does not support runtime in the browser. It should, however, work if you're looking to sandbox code in a nodejs app. – kevin.groat Jun 13 '20 at 01:36
0

With NISP you'll be able to do sandboxed evaluation.

Though the expression you write is not exactly JavaScript code, instead you'll write S-expressions. It is ideal for simple DSLs that doesn't demand extensive programming.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Kannan Ramamoorthy
  • 3,980
  • 9
  • 45
  • 63
-3
  1. Suppose you have code to execute:

     var sCode = "alert(document)";
    

    Now, suppose you want to execute it in a sandbox:

     new Function("window", "with(window){" + sCode + "}")({});
    

    These two lines when executed will fail, because "alert" function is not available from the "sandbox"

  2. And now you want to expose a member of window object with your functionality:

     new Function("window", "with(window){" + sCode + "}")({
         'alert':function(sString){document.title = sString}
     });
    

Indeed you can add quotes escaping and make other polishing, but I guess the idea is clear.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Sergey Ilinsky
  • 31,255
  • 9
  • 54
  • 56
-4

Where is this user JavaScript code coming from?

There is not much you can do about a user embedding code into your page and then calling it from their browser (see Greasemonkey). It's just something browsers do.

However, if you store the script in a database, then retrieve it and eval() it, then you can clean up the script before it is run.

Examples of code that removes all window. and document. references:

 eval(
  unsafeUserScript
    .replace(/\/\/.+\n|\/\*.*\*\/, '') // Clear all comments
    .replace(/\s(window|document)\s*[\;\)\.]/, '') // Removes window. Or window; or window)
 )

This tries to prevent the following from being executed (not tested):

window.location = 'http://example.com';
var w = window;

There are a lot of limitations you would have to apply to the unsafe user script. Unfortunately, there isn't any 'sandbox container' available for JavaScript.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dimitry
  • 6,545
  • 2
  • 20
  • 21
  • 3
    If someone is trying to do something malicious a simple regex just can't do it -- take (function(){this["loca"+"tion"]="http://example.com";})() In general if you can't trust your users (which is the case with any site on which arbitrary people can add content) blocking all js is necessary. – olliej Oct 12 '08 at 07:47
  • I've used something similar in the past. It's not perfect, but it gets you most of the way there. – Sugendran Oct 12 '08 at 09:34
  • olliej, you are right about the limitations of such a technique. How about overwriting global variables like var window = null, document = null, this = {};? – Dimitry Oct 12 '08 at 16:18
  • 1
    Dimitry Z, overwriting these variables is not allowed [in some browsers]. Also check on my solution in the list of answers - it works. – Sergey Ilinsky Oct 12 '08 at 19:00
-5

I've been working on a simplistic JavaScript sandbox for letting users build applets for my site. Although I still face some challenges with allowing DOM access (parentNode just won't let me keep things secure =/), my approach was just to redefine the window object with some of its useful/harmless members, and then eval() the user code with this redefined window as the default scope.

My "core" code goes like this... (I'm not showing it entirely ;)

function Sandbox(parent){

    this.scope = {
        window: {
            alert: function(str){
                alert("Overriden Alert: " + str);
            },
            prompt: function(message, defaultValue){
                return prompt("Overriden Prompt:" + message, defaultValue);
            },
            document: null,
            .
            .
            .
            .
        }
    };

    this.execute = function(codestring){

        // Here some code sanitizing, please

        with (this.scope) {
            with (window) {
                eval(codestring);
            }
        }
    };
}

So, I can instantiate a Sandbox and use its execute() function to get code running. Also, all new declared variables within eval'd code will ultimately bound to the execute() scope, so there will not be clashing names or messing with existing code.

Although global objects will still be accessible, those which should remain unknown to the sandboxed code must be defined as proxies in the Sandbox::scope object.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 10
    This does not sandbox anything. The evaled code can delete members and get to the global scope that way, or grab a reference to ghe global scope by doing (function () { return this; })() – Mike Samuel Sep 30 '09 at 17:52
-6

You can wrap the user's code in a function that redefines forbidden objects as parameters -- these would then be undefined when called:

(function (alert) {

alert ("uh oh!"); // User code

}) ();

Of course, clever attackers can get around this by inspecting the JavaScript DOM and finding a non-overridden object that contains a reference to the window.


Another idea is scanning the user's code using a tool like JSLint. Make sure it's set to have no preset variables (or: only variables you want), and then if any globals are set or accessed do not let the user's script be used. Again, it might be vulnerable to walking the DOM -- objects that the user can construct using literals might have implicit references to the window object that could be accessed to escape the sandbox.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
John Millikin
  • 197,344
  • 39
  • 212
  • 226
  • 3
    If the user entered window.alert instead of plain alert, they would bypass that limit. – Quentin Oct 12 '08 at 06:41
  • @Dorward: yes, hence "forbidden objects". wrunsby should decide what objects the user is not allowed to access, and place them in the parameter list. – John Millikin Oct 12 '08 at 06:47
  • There is only one object - window. If you don't block access to it, then everything is available through it. If you do block it, then the script can't access anything of its properties (since saying alert instead of window.alert just implies the window.). – Quentin Oct 12 '08 at 07:40
  • 1
    @Doward: that isn't the case you would block window.alert but alert would still work, try it. This is because window is also the global object. One would need to block window and any property or method of window you didn't want the user code to access. – AnthonyWJones Oct 12 '08 at 12:10