78

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 ); }; 
16
  • Wouldn't they still be able to send AJAX requests? Commented May 18, 2012 at 13:45
  • @SLaks The worker has XHR set to null Commented May 18, 2012 at 13:46
  • 2
    It appears that if you delete a native/host object, it will be restored in its original state. "delete XMLHttpRequest; XMLHttpRequest;" Will return the original XMLHttpRequest object. There must be a way around this :/ Commented May 18, 2012 at 14:08
  • 2
    You have the blacklist of functions, that are unsafe. It doesn't look good. What if new standard or browser defines new unsafe functions? Commented May 18, 2012 at 15:43
  • 1
    As a paranoid I would add some kind of runtime test, that your method actually blocks something on working system. Otherwise, looks okay, but I'm not an expert. Commented May 18, 2012 at 21:08

2 Answers 2

42
+100

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) ); }; })(); 
Sign up to request clarification or add additional context in comments.

22 Comments

Why are Math, and parse* functions disabled?
@Domi why do you think they are disabled
@Domi I think you are misunderstanding - this is a whitelist approach. Anything not listed is disallowed while only the things that are listed are allowed. Whitelist is better because it doesn't require updating when new APIs are implemented.
How do you know that for future APIs? :-) But I've checked it now, Object.defineProperty would throw and prevent the onmessage handler from being set up.
@Domi *Timeout and *Interval function are listed jsfiddle.net/WuhrP/2 (I am using version 33.0.1750.152)
|
7

The code currently (2014-11-07) shown in the question despite ostensibly disallowing access to XMLHttpRequest (since it is not whitelisted), still allows code to access it.

If I put the code in the question (or the accepted answer) in a web page and worker combo and execute the following code on Chrome 38:

makeWorkerExecuteSomeCode('event.target.XMLHttpRequest', function (answer) { console.log( answer ); }); 

The result is:

function XMLHttpRequest() { [native code] } 

However it does not work in FF. Bug in Chrome?

Another thing I found but which does not seem to lead very far down the rabit hole is reinstating console.log. This works on FF 31 but not Chrome 38:

makeWorkerExecuteSomeCode( 'var c = self.__proto__.__proto__.__lookupGetter__("console").call(self); c.log("FOO");', function (answer) { console.log(answer) }); 

This would log "FOO" to the console without passing through the fake console.log that the web worker provides. The code above uses self, which can be blacklisted (by removing it from the whitelist) but this and global also work. I've found that attempts to blacklist global fail on FF and Chrome: the worker dies with an error.

Note: Chrome refuses to blacklist Intl so it has to be added to the whitelist for the code to run at all.

6 Comments

Very good point concerning the hole in Chrome 38, but that is rather easy to fill: Just put a closure around the try/catch in onmessage and redefine event with var event; within the closure.
So is there any real solution?
@GregoryMagarshak Yes: WebAssembly. It allows you to run any programming language you want (even Python, or a lite version of JavaScript) inside your page. People especially like running C++ in WebAssembly. WebAssembly is great for this task as it has no access to anything other than what you feed it.
how does that solve the problem @aggregate1166877?
@GregoryMagarshak The question is about executing untrusted code in a secure way. WebAssembly effectively creates a micro-OS style environment in your page with access to nothing. You can feed it data, and get data back from it via an API you create. By running something like Elk inside WebAssembly, you have isolated JS code running inside a well-defined sandbox with a tiny memory footprint.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.