8

Background: I'm working on a framework/library to be used for a specific site in coordination with greasemonkey/userscripts. This framework/library will allow for addon support. The way it will work is an addon registers with the library listing required pages, resources, ectera and the library will wait until all critera is met to call the addon's load() function.

The Issue:In this listing of 'required stuff' I want addon devs to be able to specify javascript(as string) to be evaluated as a 'required resource'. For example 'document.getElementById("banana")'. What I want to do is semi-sandbox the evaluation of 'required resource' so the evaluation can access the window & DOM objects but is not able to directly alter them. I'd also like to make eval, and evalJS inaccessible from the sandbox.


Examples:

  • document.getElementById("banana") -> valid
  • document.getElementById("apple).id = "orange" -> invalid
  • window.grape -> valid
  • window.grape = 'potato' -> invalid
  • (someObj.applesCount > 0 ? 'some' : 'none') -> valid


What I have so far:

function safeEval(input) {

    // Remove eval and evalJS from the window:
    var e = [window.eval, window.evalJS], a;
    window.eval = function(){};
    window.evalJS = function(){};

    try {

        /* More sanition needed before being passed to eval */

        // Eval the input, stuffed into an annonomous function
        // so the code to be evalued can not access the stored
        // eval functions:
        a = (e[0])("(function(){return "+input+"}())");
    } catch(ex){}

    // Return eval and evalJS to the window:
    window.eval = e[0];
    window.evalJS = e[1];

    // Return the eval'd result
    return a;
}


Notes:
This is a Greasemonkey/userscript. I do not have direct access to alter the site, or it's javascript.
The input for safeEval() can be any valid javascript, be it a DOM query, or simple evaluations so long as it does not alter the window object or DOM.

SReject
  • 3,774
  • 1
  • 25
  • 41
  • May want to check out: [How do I safely “eval” user code in a webpage?](http://stackoverflow.com/q/6714090/402706) – Brandon Boone Sep 08 '12 at 21:45
  • Caja is nice if it's my webserver that pulls up code from 3rd parties. But greasemonkey/userscripts is client-side, and requiring yet another library/widget on a site that's already packed full of them is not a solution I'm willing to accept for this problem. (I apologize if this comes off snappy, I didn't mean it as such) – SReject Sep 08 '12 at 21:51
  • It looks like Caja would be fairly simple to use from Greasemonkey: One `@require` directive and some GM_xhr calls to their API. Not sure that Caja does what you want, though. – Brock Adams Sep 08 '12 at 22:35
  • Chrome and Opera's userscript support doesn't enforce the @require tag, sadly – SReject Sep 12 '12 at 17:17
  • Thoughts on deep-cloning `window` and `document`? That way, the properties would be alterable, but you could restore the original DOM afterwards. Could be a performance hit, though. – Platinum Azure Sep 23 '12 at 17:02
  • I thought about deep cloning, but there's a few issues. The site this greasemonkey script runs on uses alot of closures, which makes cloning impossible to say the least. The second being the site is already slow enough, having my code clone it would make it crawl. – SReject Sep 23 '12 at 21:31
  • Closely related to the topic: [Is It Possible to Sandbox JavaScript Running In the Browser?](http://stackoverflow.com/questions/195149/is-it-possible-to-sandbox-javascript-running-in-the-browser) – Akseli Palén Oct 31 '13 at 22:37

4 Answers4

7

There's no absolute way to prevent an end user or addon developer from executing specific code in JavaScript. That's why security measures in an open source language like JavaScript is said to be foolproof (as in it's only effective against fools).

That being said however let's build a sandbox security layer to prevent inexperienced developers from breaking your site. Personally I prefer using the Function constructor over eval to execute user code for the following reasons:

  1. The code is wrapped in an anonymous function. Hence it may be stored in a variable and called as many times as needed.
  2. The function always exists in the global scope. Hence it doesn't have access to the local variables in the block which created the function.
  3. The function may be passed arbitrary named parameters. Hence you may exploit this feature to pass or import modules required by the user code (e.g. jQuery).
  4. Most importantly you may set a custom this pointer and create local variables named window and document to prevent access to the global scope and the DOM. This allows you to create your own version of the DOM and pass it to the user code.

Note however that even this pattern has disadvantages. Most importantly it may only prevent direct access to the global scope. User code may still create global variables by simply declaring variables without var, and malicious code may use hacks like creating a function and using it's this pointer to access the global scope (the default behavior of JavaScript).

So let's look at some code: http://jsfiddle.net/C3Kw7/

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • For a full blown sandbox you'll need to create a scanner which sandboxes every function declared or defined inside the user code. A simple [lexer](https://github.com/aaditmshah/lexer "aaditmshah/lexer") should do the job. You'll also need to [match balanced brackets](http://codereview.stackexchange.com/questions/14532/checking-for-balanced-brackets-in-javascript "Checking for balanced brackets in JavaScript - Code Review Beta - Stack Exchange"). – Aadit M Shah Sep 23 '12 at 17:59
  • I want the code to be evaluated to be able to access the current window and DOM. What I do not want is that code to be able to alter the window or DOM. – SReject Sep 23 '12 at 21:27
  • There's no way to do that without creating your own `window` and `document` objects. In addition your custom `getElementById` method on your custom `document` object would have to return a custom `HTMLElement` object to prevent the addon developer from being able to modify the DOM. Essentially you would need to create your own DOM which is a HUMONGOUS task. You're better off simply allowing developers to make addons and then verifying them. I'm pretty sure you won't find anyone hell bent on wreaking havoc. Quite frankly I think you're wasting too much time on too small a problem. – Aadit M Shah Sep 24 '12 at 13:19
  • This question is almost two weeks old; I've moved on from it, actually. Was just wondering if there was a fairly simple way to allow for execuated code to access getters, but not setters without having to clone the DOM/Window object(s). After thinking about it, I realized it's too little benefit for too much cost. Then yesterday I figured I'd followup on replies :) – SReject Sep 24 '12 at 17:16
0

If all you want is a simple getter, program one instead of trying to eval anything.

function get(input) {
    // if input is a string, it will be used as a DOM selector
    // if it is an array (of strings), they will access properties of the global object
    if (typeof input == "string")
        return document.querySelector(input);
    else if (Array.isArray(input)) {
        var res = window;
        for (var i=0; i<input.length && typeof input[i] == "string" && res; i++)
            res = res[input[i]];
        return res;
    }
    return null;
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • It's not a simple documentQuery. I want any javascript to be accessible/executable, but I do not want that code to alter the window object. Something like `safeEval('8*9')` would be valid. – SReject Sep 23 '12 at 21:25
  • That's impossible. You'd like to allow `document.getElementById("x").getAttribute("href")`, but prevent `document.getElementById("x").removeAttribute("href")`? – Bergi Sep 24 '12 at 08:41
0

You can do something like this: http://jsfiddle.net/g68NP/

Problem is that you'll have to add a lot of code to protect every property, every native method, etc. The meat of the code really comes down to using __defineGetter__, whose support is limited. Since you're probably not running this on IE, you should be fine.

EDIT: http://jsfiddle.net/g68NP/1/ This code will make all properties read-only. The use of hasOwnProperty() may or may not be desirable.

In case JSFiddle goes down:

function safeEval(input) {
    // Remove eval and evalJS from the window:
    var e = [window.eval, window.evalJS, document.getElementById], a;
    window.eval = function(){};
    window.evalJS = function(){};
    document.getElementById = function (id) {
        var elem = (e[2]).call(document, id);
        for (var prop in elem) {
            if (elem.hasOwnProperty(prop)) {
                elem.__defineGetter__(prop, function () {
                    return (function (val) {
                        return val;
                    }(elem[prop]));
                });
            }                
        }
        return elem;
    };

    try {
        /* More sanition needed before being passed to eval */

        // Eval the input, stuffed into an annonomous function
        // so the code to be evalued can not access the stored
        // eval functions:
        a = (e[0])("(function(){return " + input + "}())");
    } catch(ex){}

    // Return eval and evalJS to the window:
    window.eval = e[0];
    window.evalJS = e[1];
    document.getElementById = e[2];

    // Return the eval'd result
    return a;
}
John Kurlak
  • 6,594
  • 7
  • 43
  • 59
  • It's not a simple documentQuery. I want any javascript to be accessible/executable, but I do not want that code to alter the window object. Something like `safeEval('8*9')` would be valid. – SReject Sep 23 '12 at 21:24
  • @SReject How is any JS code not valid in my safeEval? safeEval('8*9') returns 72 just fine. Everything else works just fine too, except for changing the values of properties to elements retrieved by document.getElementById. – John Kurlak Sep 24 '12 at 01:18
  • @John What's the point of removing eval()? It is easy to re-implement. Also, if this is FF you probably want to hide Components, since you can use Components.lookupMethod(document, 'getElementById') to get original implementation. Also probably worth adding "use strict" to your function string-interpolation, and also setting the __proto__ of the running function to something like (x={},x.__proto__=null,return x). This won't help you in FF but will certainly prevent some naughty things in chrome. – Joe V Apr 06 '13 at 18:56
  • @JoeV The point of removing eval() is to fulfill the requirement "I'd also like to make eval, and evalJS inaccessible from the sandbox." Agree with all your points though. – John Kurlak Apr 06 '13 at 19:01
0

I know this is an old post, but I just want to share an upgraded version of the Aadit M Shah solution, that seems to really sandbox without any way to access window (or window children): http://jsfiddle.net/C3Kw7/20/

// create our own local versions of window and document with limited functionality

var locals = {
    window: {
    },
    document: {
    }
};

var that = Object.create(null); // create our own this object for the user code
var code = document.querySelector("textarea").value; // get the user code
var sandbox = createSandbox(code, that, locals); // create a sandbox

sandbox(); // call the user code in the sandbox

function createSandbox(code, that, locals) {
    code = '"use strict";' + code;
    var params = []; // the names of local variables
    var args = []; // the local variables

    var keys = Object.getOwnPropertyNames( window ),
    value;

    for( var i = 0; i < keys.length; ++i ) {
        //console.log(keys[i]);
        locals[keys[i]] = null;  
    }

    delete locals['eval'];
    delete locals['arguments'];

    locals['alert'] = window.alert; // enable alert to be used

    for (var param in locals) {
        if (locals.hasOwnProperty(param)) {
            args.push(locals[param]);
            params.push(param);
        }
    }

    var context = Array.prototype.concat.call(that, params, code); // create the parameter list for the sandbox
    //console.log(context);
    var sandbox = new (Function.prototype.bind.apply(Function, context)); // create the sandbox function
    context = Array.prototype.concat.call(that, args); // create the argument list for the sandbox

    return Function.prototype.bind.apply(sandbox, context); // bind the local variables to the sandbox
}
biggbest
  • 608
  • 1
  • 5
  • 9
  • 3
    Just a note, this is not secure and can be easily circumvented by doing this `var global = Function("return this")(); global.alert(global.document);` – Anvaka Jun 23 '15 at 23:57
  • 1
    @Anvaka When I run that in this sandbox I get `Uncaught TypeError: Function is not a function`. – Ben J Aug 22 '18 at 03:29
  • This almost works perfectly, but if my page has an element with `id="test"` then the code can access that dom node via simply `createSandbox('test', Object.create(null), {})`. – Adam Keenan Jun 25 '21 at 06:08
  • Is this still working? I get an error while trying it on Chrome 111+ – adelriosantiago Apr 13 '23 at 15:21