Tornando o WebWorkers um ambiente seguro

Em uma busca para ter uma interface capaz de executar código JavaScript arbitrário dentro do navegador, sem ter uma falha de segurança do tamanho de uma piada típica de yo-mama, Esailija propôs o uso de Web Workers . Eles são executados em um ambiente semi-sandbox (sem access DOM e já dentro do navegador) e podem ser eliminados para que o usuário não possa colocá-los em um loop infinito.

Aqui está o exemplo que ele trouxe: http://tuohiniemi.fi/~runeli/petka/workertest.html (abra seu console)

jsfiddle (Google Chrome apenas)

Agora, isso parece uma boa solução; no entanto, é um completo (ou se aproximando completo)? Existe alguma coisa óbvia faltando?

A coisa toda (como é ligada a um bot) pode ser encontrada no github: worker , evaluator

a Principal:

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 ); }); 

trabalhador:

 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 ); }; 

    O código atual (listado abaixo) agora está em uso na sala de bate-papo do Stackoverflow javascript há algum tempo e até agora o problema mais difícil foi Array(5000000000).join("adasdadadasd") quebrando instantaneamente algumas guias do navegador para mim quando eu estava executando o executor de código bot. O Monkeypatching Array.prototype.join parece ter corrigido isso e o tempo máximo de execução de 50 ms funcionou para qualquer outra tentativa de prejudicar a memory ou travar o navegador.

     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) ); }; })(); 

    O código atualmente (2014-11-07) mostrado na questão, apesar de aparentemente não permitir o access ao XMLHttpRequest (uma vez que não está na lista de permissions), ainda permite que o código o acesse.

    Se eu colocar o código na pergunta (ou a resposta aceita) em uma página da Web e combinação de trabalho e executar o seguinte código no Chrome 38:

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

    O resultado é:

     function XMLHttpRequest() { [native code] } 

    No entanto, não funciona em FF. Bug no Chrome?

    Outra coisa que eu encontrei, mas que não parece levar muito longe no buraco rabit, é restabelecer o console.log . Isso funciona no FF 31, mas não no Chrome 38:

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

    Isso registraria "FOO" no console sem passar pelo console.log que o web worker fornece. O código acima usa self , que pode ser colocado na lista negra (removendo-o da whitelist), mas this e o global também funcionam. Descobri que as tentativas de bloquear global falha global na FF e no Chrome: o funcionário morre com um erro.

    Nota: o Chrome recusa-se a colocar o Intl lista negra, pelo que tem de ser adicionado à lista de autorizações para que o código seja executado.