Делаем WebWorkers безопасной средой

В стремлении иметь интерфейс, способный запускать произвольный код javascript внутри браузера, не имея дыры в безопасности размером с типичную шутку о маме, Esailija предложила использовать Web Workers. Они работают в полуизолированной среде (без доступа к DOM и уже внутри браузера) и могут быть уничтожены, чтобы пользователь не мог запустить их в бесконечный цикл.

Вот пример, который он привел: http://tuohiniemi.fi/~runeli/petka/workertest.html (откройте консоль)

jsfiddle (только Google Chrome)

Теперь это кажется хорошим решением; однако является ли оно полным (или приближающимся к завершению)? Чего-то очевидного не хватает?

Все это (так как оно подключено к боту) можно найти на github: работник, оценщик

основной:

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

работник:

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

person Zirak    schedule 18.05.2012    source источник
comment
Разве они не смогут отправлять запросы AJAX?   -  person SLaks    schedule 18.05.2012
comment
@SLaks Рабочий XHR установлен на null   -  person Esailija    schedule 18.05.2012
comment
@SLaks Вы можете сами вручную удалить кучу небезопасных свойств, как показано в примере.   -  person Zirak    schedule 18.05.2012
comment
Похоже, что если вы delete нативный/хост-объект, он будет восстановлен в исходном состоянии. "delete XMLHttpRequest; XMLHttpRequest;" вернет исходный объект XMLHttpRequest. Должен быть способ обойти это :/   -  person Esailija    schedule 18.05.2012
comment
Nvm, обходной путь был предложен @copy. Редактирование в   -  person Esailija    schedule 18.05.2012
comment
Честно говоря, это гениально. Я хотел бы дать вам больше репутации для этого.   -  person Snuffleupagus    schedule 18.05.2012
comment
У вас есть черный список небезопасных функций. Это не выглядит хорошо. Что, если новый стандарт или браузер определяет новые небезопасные функции?   -  person zch    schedule 18.05.2012
comment
Я думаю, вам нужен белый список - удалите все функции, кроме тех, о безопасности которых вы знаете.   -  person zch    schedule 18.05.2012
comment
@zch Я только что заметил это, когда перечислил глобальный объект в worker. Я делаю подход белого списка. Спасибо   -  person Esailija    schedule 18.05.2012
comment
@zch сейчас мы используем подход белого списка, как он выглядит?   -  person Esailija    schedule 18.05.2012
comment
Как параноик, я бы добавил какой-то тест во время выполнения, что ваш метод действительно блокирует что-то в рабочей системе. В остальном выглядит нормально, но я не эксперт.   -  person zch    schedule 19.05.2012
comment
Как насчет использования чего-то вроде: code.google.com/p/google-caja. ?   -  person Ido Green    schedule 23.05.2012
comment
Может быть, стоит запустить виртуальную машину, такую ​​как continuum?   -  person Florian Margaine    schedule 31.05.2013
comment
@IdoGreen Для этого потребуется включить огромную внешнюю структуру. Родное решение является предпочтительным.   -  person Zirak    schedule 31.05.2013
comment
@FlorianMargaine Это предпочтительное обучение, но с огромным снижением производительности.   -  person Zirak    schedule 31.05.2013
comment
Какой точный обходной путь был добавлен, чтобы предотвратить отмену ключевого слова удаления? @Эсаилия   -  person Gregory Magarshak    schedule 26.12.2018


Ответы (2)


Текущий код (перечисленный ниже) уже некоторое время используется в чате javascript Stackoverflow, и до сих пор самой сложной проблемой был Array(5000000000).join("adasdadadasd") мгновенный сбой некоторых вкладок браузера для меня, когда я запускал бота-исполнителя кода. Monkeypatching Array.prototype.join, кажется, исправил это, и максимальное время выполнения 50 мс работало для любой другой попытки захвата памяти или сбоя браузера.

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

})();
person Esailija    schedule 29.05.2012
comment
Почему функции Math и parse* отключены? - person Domi; 10.04.2014
comment
@Доми, почему ты думаешь, что они отключены? - person Esailija; 10.04.2014
comment
@Esailija Пока я нашел только междоменный Math.random () предсказание. Но поскольку злоумышленник не может общаться с внешним миром, это кажется безобидным. - person Domi; 10.04.2014
comment
Кстати, я также запрещаю XMLHttpRequest, Worker, WebSocket и importScripts. Как вы, ребята, справляетесь с этим? - person Domi; 10.04.2014
comment
@Domi Я думаю, вы неправильно понимаете - это подход к белому списку. Все, что не указано в списке, запрещено, а разрешено только то, что указано в списке. Белый список лучше, потому что он не требует обновления при внедрении новых API. - person Esailija; 10.04.2014
comment
Что произойдет, если любое из небезопасных свойств уже не настраивается? - person Bergi; 10.04.2014
comment
@Bergi, этого не происходит в случае использования - person Esailija; 10.04.2014
comment
Откуда вы знаете, что для будущих API? :-) Но я сейчас проверил, Object.defineProperty бросал и не давал установить обработчик onmessage. - person Bergi; 10.04.2014
comment
Я попробовал это в Chrome, и он сообщил, что не может переопределить свойство Международный. Добавление в белый список помогло. - person Domi; 11.04.2014
comment
Еще один глупый вопрос: Чем опасен допуск console? Является ли это также частью того, чтобы сделать его перспективным, или его можно использовать в настоящее время? - person Domi; 11.04.2014
comment
@Domi Я не думаю, что в веб-воркере есть console - person Esailija; 11.04.2014
comment
@Esailija есть, по крайней мере, в Chrome и IE. Пользуюсь уже довольно давно. Я предполагаю, что он работает в своем собственном потоке. - person Domi; 12.04.2014
comment
Плохая новость @Esailija: в Chrome есть ошибка (или функция?), из-за которой Object.getOwnPropertyNames не перечисляет целую кучу глобальных переменных, включая все функции *Timeout и *Interval. Этот JSFiddle показывает довольно длинный список таких глобальных переменных (по крайней мере, в Chrome). Этот скрипт не будет работать в IE, потому что он использует встроенные рабочие процессы (по какой-то глупой причине текущая версия IE не позволяет запускать рабочие процессы с Blob URL-адресов). - person Domi; 13.04.2014
comment
@Доми, это не ошибка, и их находит Object.getOwnPropertyNames( global.__proto__ ) - person Esailija; 14.04.2014
comment
@Esailija Это неправда. global.__proto__ находит только две дополнительные глобальные переменные в моем Chrome: constructor и postMessage. Остальное по-прежнему отсутствует, включая функции *Timeout и *Interval. см. этот обновленный JSFiddle для доказательства. - person Domi; 14.04.2014
comment
@Domi *Функции Timeout и *Interval перечислены jsfiddle.net/WuhrP/2 (я использую версия 33.0.1750.152) - person Esailija; 14.04.2014
comment
@Esailija Извините, тогда в моем коде, вероятно, была ошибка. В любом случае, не могли бы вы взглянуть на этот вопрос ? - person Domi; 15.04.2014
comment
Есть ли способ для кода в WebWorker отменить отключение, например, с помощью ключевого слова delete? - person Gregory Magarshak; 26.12.2018

Код, показанный в настоящее время (2014-11-07) в вопросе, несмотря на то, что он якобы запрещает доступ к XMLHttpRequest (поскольку он не внесен в белый список), по-прежнему позволяет коду получить к нему доступ.

Если я помещу код в вопрос (или принятый ответ) в комбинацию веб-страницы и рабочего и выполню следующий код в Chrome 38:

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

Результат:

function XMLHttpRequest() { [native code] } 

Однако это не работает в FF. Ошибка в Хроме?

Еще одна вещь, которую я нашел, но которая, похоже, не ведет очень далеко вниз, - это восстановление console.log. Это работает на FF 31, но не на Chrome 38:

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

Это позволит вывести "FOO" на консоль без прохождения через поддельный console.log, который предоставляет веб-воркер. В приведенном выше коде используется self, который можно занести в черный список (удалив его из белого списка), но this и global также работают. Я обнаружил, что попытки внести global в черный список на FF и Chrome терпят неудачу: рабочий процесс умирает с ошибкой.

Примечание. Chrome отказывается заносить Intl в черный список, поэтому его необходимо добавить в белый список, чтобы код вообще работал.

person Louis    schedule 07.11.2014
comment
Очень хорошее замечание по поводу дыры в Chrome 38, но ее довольно легко заполнить: просто поместите замыкание вокруг try/catch в onmessage и переопределите event с var event; внутри замыкания. - person heinob; 07.11.2014
comment
Так есть ли реальное решение? - person Gregory Magarshak; 26.12.2018