Search code examples
javascriptweb-worker

Making WebWorkers a safe environment


In a quest to have an interface capable of running arbitrary javascript code inside the browser, without having a security hole the size of a typical yo-mama joke, Esailija proposed using Web Workers. They run in a semi-sandboxed environment (no DOM access and already inside the browser) and can be killed so the user can't put them in an infinite loop.

Here's the example he brought up: http://tuohiniemi.fi/~runeli/petka/workertest.html (open your console)

jsfiddle (Google chrome only)

Now, this seems like a good solution; however, is it a complete (or approaching complete) one? Is there anything obvious missing?

The entire thing (as it's hooked up to a bot) can be found on github: worker, evaluator

main:

workercode = "worker.js";

function makeWorkerExecuteSomeCode( code, callback ) {
    var timeout;

    code = code + "";
    var worker = new Worker( workercode );

    worker.addEventListener( "message", function(event) {
        clearTimeout(timeout);
        callback( event.data );
    });

    worker.postMessage({
        code: code
    });

    timeout = window.setTimeout( function() {
        callback( "Maximum execution time exceeded" );
        worker.terminate();
    }, 1000 );
}

makeWorkerExecuteSomeCode( '5 + 5', function(answer){
    console.log( answer );
});

makeWorkerExecuteSomeCode( 'while(true);', function(answer){
    console.log( answer );
});

var kertoma = 'function kertoma(n){return n === 1 ? 1 : n * kertoma(n-1)}; kertoma(15);';

makeWorkerExecuteSomeCode( kertoma, function(answer){
    console.log( answer );
});

worker:

var global = this;

/* Could possibly create some helper functions here so they are always available when executing code in chat?*/

/* Most extra functions could be possibly unsafe */

    var wl = {
        "self": 1,
        "onmessage": 1,
        "postMessage": 1,
        "global": 1,
        "wl": 1,
        "eval": 1,
        "Array": 1,
        "Boolean": 1,
        "Date": 1,
        "Function": 1,
        "Number" : 1,
        "Object": 1,
        "RegExp": 1,
        "String": 1,
        "Error": 1,
        "EvalError": 1,
        "RangeError": 1,
        "ReferenceError": 1,
        "SyntaxError": 1,
        "TypeError": 1,
        "URIError": 1,
        "decodeURI": 1,
        "decodeURIComponent": 1,
        "encodeURI": 1,
        "encodeURIComponent": 1,
        "isFinite": 1,
        "isNaN": 1,
        "parseFloat": 1,
        "parseInt": 1,
        "Infinity": 1,
        "JSON": 1,
        "Math": 1,
        "NaN": 1,
        "undefined": 1
    };

    Object.getOwnPropertyNames( global ).forEach( function( prop ) {
        if( !wl.hasOwnProperty( prop ) ) {
            Object.defineProperty( global, prop, {
                get : function() {
                    throw new Error( "Security Exception: cannot access "+prop);
                    return 1;
                }, 
                configurable : false
            });    
        }
    });

    Object.getOwnPropertyNames( global.__proto__ ).forEach( function( prop ) {
        if( !wl.hasOwnProperty( prop ) ) {
            Object.defineProperty( global.__proto__, prop, {
                get : function() {
                    throw new Error( "Security Exception: cannot access "+prop);
                    return 1;
                }, 
                configurable : false
            });    
        }
    });




onmessage = function( event ) {
    "use strict";
    var code = event.data.code;
    var result;
    try {
        result = eval( '"use strict";\n'+code );
    }
    catch(e){
        result = e.toString();
    }
    postMessage( "(" + typeof result + ")" + " " + result );
};

Solution

  • The current code (listed below) has been now in use in the Stackoverflow javascript chat room for a while and so far the toughest problem was Array(5000000000).join("adasdadadasd") instantly crashing some browser tabs for me when I was running the code executor bot. Monkeypatching Array.prototype.join seems to have fixed this and the maximum execution time of 50ms has worked for any other attempt to hog memory or crash the browser.

    var global = this;
    
    /* Could possibly create some helper functions here so they are always available when executing code in chat?*/
    
    /* Most extra functions could be possibly unsafe */
    
    var wl = {
        "self": 1,
        "onmessage": 1,
        "postMessage": 1,
        "global": 1,
        "wl": 1,
        "eval": 1,
        "Array": 1,
        "Boolean": 1,
        "Date": 1,
        "Function": 1,
        "Number" : 1,
        "Object": 1,
        "RegExp": 1,
        "String": 1,
        "Error": 1,
        "EvalError": 1,
        "RangeError": 1,
        "ReferenceError": 1,
        "SyntaxError": 1,
        "TypeError": 1,
        "URIError": 1,
        "decodeURI": 1,
        "decodeURIComponent": 1,
        "encodeURI": 1,
        "encodeURIComponent": 1,
        "isFinite": 1,
        "isNaN": 1,
        "parseFloat": 1,
        "parseInt": 1,
        "Infinity": 1,
        "JSON": 1,
        "Math": 1,
        "NaN": 1,
        "undefined": 1
    };
    
    Object.getOwnPropertyNames( global ).forEach( function( prop ) {
        if( !wl.hasOwnProperty( prop ) ) {
            Object.defineProperty( global, prop, {
                get : function() {
                    throw "Security Exception: cannot access "+prop;
                    return 1;
                }, 
                configurable : false
            });    
        }
    });
    
    Object.getOwnPropertyNames( global.__proto__ ).forEach( function( prop ) {
        if( !wl.hasOwnProperty( prop ) ) {
            Object.defineProperty( global.__proto__, prop, {
                get : function() {
                    throw "Security Exception: cannot access "+prop;
                    return 1;
                }, 
                configurable : false
            });    
        }
    });
    
    Object.defineProperty( Array.prototype, "join", {
    
        writable: false,
        configurable: false,
        enumerable: false,
    
        value: function(old){
            return function(arg){
                if( this.length > 500 || (arg && arg.length > 500 ) ) {
                    throw "Exception: too many items";
                }
    
                return old.apply( this, arguments );
            };
        }(Array.prototype.join)
    
    });
    
    
    (function(){
        var cvalues = [];
    
        var console = {
            log: function(){
                cvalues = cvalues.concat( [].slice.call( arguments ) );
            }
        };
    
        function objToResult( obj ) {
            var result = obj;
            switch( typeof result ) {
                case "string":
                    return '"' + result + '"';
                    break;
                case "number":
                case "boolean":
                case "undefined":
                case "null":
                case "function":
                    return result + "";
                    break;
                case "object":
                    if( !result ) {
                        return "null";
                    }
                    else if( result.constructor === Object || result.constructor === Array ) {
                        var type = ({}).toString.call( result );
                        var stringified;
                        try {
                            stringified = JSON.stringify(result);
                        }
                        catch(e) {
                            return ""+e;
                        }
                        return type + " " + stringified;
                    }
                    else {
                        return ({}).toString.call( result );
                    }
                    break;
    
            }
    
        }
    
        onmessage = function( event ) {
            "use strict";
            var code = event.data.code;
            var result;
            try {
                result = eval( '"use strict";\n'+code );
            }
            catch(e) {
                postMessage( e.toString() );
                return;
            }
            result = objToResult( result );
            if( cvalues && cvalues.length ) {
                result = result + cvalues.map( function( value, index ) {
                    return "Console log "+(index+1)+":" + objToResult(value);
                }).join(" ");
            }
            postMessage( (""+result).substr(0,400) );
        };
    
    })();