WebWorker zu einer sicheren Umgebung machen


68

In dem Bestreben, eine Schnittstelle zu haben, die beliebigen Javascript-Code im Browser ausführen kann, ohne eine Sicherheitslücke von der Größe eines typischen Jo-Mama-Witzes zu haben, schlug Esailija die Verwendung von Web Workers vor . Sie werden in einer Semi-Sandbox-Umgebung ausgeführt (kein DOM-Zugriff und bereits im Browser) und können getötet werden, sodass der Benutzer sie nicht in eine Endlosschleife stellen kann.

Hier ist das Beispiel, das er angesprochen hat: http://tuohiniemi.fi/~runeli/petka/workertest.html (öffnen Sie Ihre Konsole)

jsfiddle (nur Google Chrome)

Dies scheint eine gute Lösung zu sein. Ist es jedoch vollständig (oder fast vollständig)? Fehlt etwas Offensichtliches?

Das Ganze (da es an einen Bot angeschlossen ist) ist auf github zu finden: Arbeiter , Bewerter

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

Arbeiter:

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

Würden sie nicht immer noch AJAX-Anfragen senden können?
SLaks

@ Slaks Der Arbeiter hat XHR eingestellt aufnull
Esailija

2
Wenn Sie deleteein natives / Host-Objekt sind, wird es anscheinend in seinem ursprünglichen Zustand wiederhergestellt. "delete XMLHttpRequest; XMLHttpRequest;"Gibt das ursprüngliche XMLHttpRequest-Objekt zurück. Es muss einen Weg geben, dies zu
umgehen

1
Sie haben die schwarze Liste der Funktionen, die unsicher sind. Es sieht nicht gut aus. Was ist, wenn ein neuer Standard oder Browser neue unsichere Funktionen definiert?
Zch

1
Als Paranoid würde ich eine Art Laufzeit-Test hinzufügen, dass Ihre Methode tatsächlich etwas auf dem funktionierenden System blockiert. Ansonsten sieht es okay aus, aber ich bin kein Experte.
Zch

Antworten:


37

Der aktuelle Code (unten aufgeführt) wird nun seit einiger Zeit im Javascript-Chatroom von Stackoverflow verwendet. Bisher war das größte Problem das Array(5000000000).join("adasdadadasd")sofortige Abstürzen einiger Browser-Registerkarten für mich, als ich den Code Executor-Bot ausführte. Monkeypatching Array.prototype.joinscheint dies behoben zu haben und die maximale Ausführungszeit von 50 ms hat für jeden anderen Versuch funktioniert, den Speicher zu belasten oder den Browser zum Absturz zu bringen.

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

})();

Herzlichen Glückwunsch :) Dies ist so nah wie es nur geht
Benjamin Gruenbaum

Warum sind die Funktionen Math und Parse * deaktiviert?
Domi

1
@Domi Ich denke, Sie haben ein Missverständnis - dies ist ein Whitelist-Ansatz. Alles, was nicht aufgeführt ist, ist nicht zulässig, während nur die aufgeführten Elemente zulässig sind. Die Whitelist ist besser, da sie nicht aktualisiert werden muss, wenn neue APIs implementiert werden.
Esailija

2
Woher wissen Sie das für zukünftige APIs? :-) Aber ich habe es jetzt überprüft, Object.definePropertywürde werfen und verhindern, dass der onmessageHandler eingerichtet wird.
Bergi

1
@Domi * Timeout und * Interval Funktion sind aufgelistet jsfiddle.net/WuhrP/2 (Ich verwende Version 33.0.1750.152)
Esailija

5

Der Code, der derzeit (07.11.2014) in der Frage angezeigt wird, obwohl er angeblich den Zugriff auf XMLHttpRequestnicht zulässt (da er nicht auf der Whitelist steht), ermöglicht dem Code weiterhin den Zugriff darauf.

Wenn ich den Code in die Frage (oder die akzeptierte Antwort) in eine Kombination aus Webseite und Worker eingefügt und den folgenden Code in Chrome 38 ausgeführt habe:

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

Das Ergebnis ist:

function XMLHttpRequest() { [native code] } 

Es funktioniert jedoch nicht in FF. Fehler in Chrome?

Eine andere Sache, die ich gefunden habe, die aber nicht sehr weit in das Kaninchenloch zu führen scheint, ist die Wiedereinsetzung console.log. Dies funktioniert auf FF 31, aber nicht auf Chrome 38:

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

Dies würde sich "FOO"an der Konsole anmelden , ohne die console.logvom Web-Worker bereitgestellte Fälschung zu durchlaufen . Der obige Code Anwendungen self, die schwarze Liste gesetzt werden kann (indem sie sie von der weißen Liste zu entfernen) , sondern thisund globalauch Arbeit. Ich habe festgestellt, dass Versuche, eine schwarze Liste globalauf FF und Chrome zu erstellen, fehlschlagen: Der Worker stirbt mit einem Fehler.

Hinweis: Chrome weigert sich, eine schwarze Liste Intlzu erstellen, daher muss sie zur Whitelist hinzugefügt werden, damit der Code überhaupt ausgeführt werden kann.


Sehr guter Punkt in Bezug auf das Loch in Chrome 38, aber das ist ziemlich einfach zu füllen: Nur eine Schließung setzen um die try/catchin onmessageund neu zu definieren eventmit var event;im Verschluss.
Heinob

Gibt es also eine echte Lösung?
Gregory Magarshak
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.