views:

2815

answers:

8

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?

A: 

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

John Millikin
If the user entered window.alert instead of plain alert, they would bypass that limit.
David Dorward
@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
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.).
David Dorward
@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
A: 

Where is this user JavaScript 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, http://www.greasespot.net/). 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://mydomain.com';
var w = window  ;

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

Dimitry Z
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
I've used something similar in the past. It's not perfect, but it gets you most of the way there.
Sugendran
olliej, you are right about the limitations of such a technique. How about overwriting global variables like <code>var window = null, document = null, this = {};</code>?
Dimitry Z
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
+1  A: 

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

In general no degree of regex, etc can safely sanitise arbitrary user provided javascript as it degenerates to the halting problem :-/

olliej
+10  A: 

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

Darius Bacon
+3  A: 

1) Suppose you have a 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.

Sergey Ilinsky
+3  A: 

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.

Simon Lieschke
A: 

I've been working on a simplistic js 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 instance a Sandbox and use its execute() 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 accesible, those which should remain unknown to the sandboxed code must be defined as proxies in the Sandbox::scope object.

Hope this works for you.

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
+1  A: 

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