Защита JavaScript-функции eval

Мы хотим предоставить нашим пользователям возможность выполнять самостоятельно созданный код JavaScript в нашем приложении. Для этого нам нужно использовать eval для оценки кода. Чтобы уменьшить все проблемы безопасности до минимума (если не ноль), наша идея состоит в том, чтобы предотвратить использование какой-либо функции window или document в коде. Так что нет XMLHttpRequest или чего-то подобного.

Это код:

function secure_eval(s) {
    var ret;

    (function(){
        var copyXMLHttpRequest = XMLHttpRequest; // save orginal function in copy

        XMLHttpRequest = undefined; // make orignal function unavailable

        (function() {
            var copyXMLHttpRequest; // prevent access to copy

            try {
                ret = eval(s)
            } catch(e) {
                console.log("syntax error or illegal function used");
            }

        }())
        XMLHttpRequest = copyXMLHttpRequest; // restore original function
    }())
    return ret;
}

Это работает следующим образом:

secure_eval('new XMLHttpRequest()'); // ==> "illegal function used"

Теперь у меня есть несколько вопросов:

  • Является ли этот шаблон правильным способом для обеспечения безопасности eval?
  • Какие функции window и document являются теми, которые считаются вредными?
  • Чтобы отправить вопрос 2. Я попытался замаскировать все (родные) функции window Но я не могу их перечислить:

Здесь не отображается XMLHttpRequest:

for( var x in window) {
    if( window[x] instanceof Function) {
        console.log(x);
    }
}

Есть ли способ получить список всех нативных функций window и document?

EDIT:

Одна из моих идей - выполнить eval внутри Worker и предотвратить доступ к XMLHttpRequest и document.createElement (см. мое решение выше). Это имело бы (на мой взгляд) следующие последствия:

  • нет доступа к оригиналу document
  • нет доступа к оригиналу window
  • нет возможности связываться с внешними ресурсами (без аякса, без скриптов)

Вы видите какие-либо недостатки или утечки здесь?

EDIT2:

Тем временем я нашел этот вопрос, ответ на который решает многие из моих проблем, а также пару вещей, о которых я даже не думал (т.е. блокировка браузера с "while(true){}".

Ответ 1

Ваш код фактически не предотвращает использование XMLHttpRequest. Я могу создать объект XMLHttpRequest с помощью этих методов:

secure_eval("secure_eval = eval"); // Yep, this completely overwrites secure_eval.
secure_eval("XMLHttpRequest()");

Или:

secure_eval("new (window.open().XMLHttpRequest)()")

Или:

secure_eval("new (document.getElementById('frame').contentWindow.XMLHttpRequest)()")

Этот третий метод основан на наличии iframe в HTML-странице страницы, который кто-то может добавить, манипулируя DOM в своем браузере. Я делаю такие манипуляции время от времени с помощью Greasemonkey, чтобы удалить раздражения или исправить сломанные графические интерфейсы.

Мне потребовалось около 5 минут, чтобы понять, и я ни в коем случае не являюсь гуру безопасности. И это только дыры, которые я смог найти быстро, возможно, есть другие, о которых я не знаю. Урок здесь состоит в том, что действительно действительно сложно защитить код через eval.

Использование рабочего

Хорошо, поэтому использование Worker для запуска кода будет заботиться о втором и третьем случаях выше, потому что нет окна, доступного в Worker. И... хм.. 1-й случай может быть обработан путем затенения secure_eval внутри его области. Конец истории? Если только...

Если я помещаю secure_eval внутри веб-рабочего и запускаю следующий код, я могу повторно найти XMLHttpRequest:

secure_eval("var old_log = console.log; console.log = function () { foo = XMLHttpRequest; old_log.apply(this, arguments); };");
console.log("blah");
console.log(secure_eval("foo"));

Принцип заключается в том, чтобы переопределить функцию, которая используется вне secure_eval, для захвата XMLHttpRequest путем назначения ее переменной, которая будет намеренно просочиться в глобальное пространство рабочего, дождитесь, пока эта функция будет использована рабочим вне secure_eval, а затем возьмите сохраненное значение. Первый console.log выше имитирует использование измененной функции вне secure_eval, а второй console.log показывает, что значение было зафиксировано. Я использовал console.log, потому что почему бы и нет? Но действительно любая функция в глобальном пространстве может быть изменена следующим образом.

Собственно, зачем ждать, пока работник может использовать какую-то функцию, с которой мы подделывали? Здесь еще один, более быстрый и быстрый способ доступа XMLHttpRequest:

secure_eval("setTimeout(function () { console.log(XMLHttpRequest);}, 0);");

Даже в рабочем (с нетронутым console.log) это приведет к выводу фактического значения XMLHttpRequest на консоль. Я также отмечу, что значение this внутри функции, переданной в setTimeout, является объектом глобальной области видимости (т.е. window, если не у рабочего, или self у рабочего), не затронутым каким-либо изменением переменных.

Что относительно другого вопроса, упомянутого в этом вопросе?

Как насчет решения здесь? Гораздо лучше, но в Chrome 38 есть еще дыра:

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

Это покажет:

function XMLHttpRequest() { [native code] }

Опять же, я не являюсь гуру безопасности или взломщиком, склоненным к неприятностям. Вероятно, есть еще много способов, о которых я не думаю.

Ответ 2

Я постараюсь ответить на ваши вопросы в порядке.

Является ли этот шаблон правильным способом для обеспечения безопасности eval?

Эта часть слегка субъективна. Я не вижу серьезных недостатков безопасности. Я попробовал несколько способов доступа к XMLHttpRequest, но я не мог:

secure_eval('XMLHttpRequest')
secure_eval('window.XMLHttpRequest')
secure_eval('eval("XMLHttpRequest")()')
secure_eval('window.__proto__.XMLHttpRequest') // nope, it not inherited

Однако, это будет много, если вы хотите, чтобы в черный список было больше вещей.

Какие функции window и document являются теми, которые считаются вредными?

Это зависит от того, что вы считаете "вредным". Это плохо, если DOM доступен вообще? Или как насчет уведомлений рабочего стола WebKit или синтеза речи?

Вам нужно будет решить это на основе вашего конкретного варианта использования.

Чтобы отправить вопрос 2. Я попытался скрыть все (родные) функции window, но я не могу их перечислить:

Это потому, что большинство методов не перечислимы. Для перечисления вы можете использовать Object.getOwnPropertyNames(window):

var globals = Object.getOwnPropertyNames(window);
for (var i = 0; i < globals.length; i++) {
    if( window[globals[i]] instanceof Function) {
        console.log(globals[i]);
    }
}

Одна из моих идей - выполнить eval внутри Worker и предотвратить доступ к XMLHttpRequest и document.createElement (см. мое решение выше).

Это звучит неплохо.

Ответ 3

Я наткнулся на действительно красивую статью в блоге о пресловутой Eval здесь. В статье подробно обсуждается. Вы не сможете устранить все проблемы безопасности, но вы можете предотвратить атаки Cross- Script, создавая токены для ввода. Это теоретически предотвратило бы вредоносность вредоносного кода.

Единственное ваше препятствие - это атаки "Человек-в-среднем". Я не уверен, что это будет возможно, так как вы не можете доверять вводам и выводам.

Mozilla Developer Network явно заявляет:

eval() - опасная функция, которая выполняет переданный код с привилегиями вызывающего абонента. Если вы запустите eval() со строкой которые могут быть затронуты злонамеренной стороной, вы можете запустить вредоносный код на компьютере пользователя с разрешениями вашего веб-страница/расширение. Что еще более важно, сторонний код может видеть область действия, в которой был вызван eval(), что может привести к возможным атакам в которых аналогичная функция не подвержена.

eval() также обычно медленнее, чем альтернативы, поскольку он должен вызывать JS-интерпретатор, в то время как многие другие конструкции оптимизированы современными двигателями JS.

Существуют более безопасные (и более быстрые!) альтернативы eval() для обычных потребительные случаи.

Я немного против Eval и действительно стараюсь использовать его, когда это оправдано.

Ответ 4

Был давным-давно такой вопрос. Поэтому я отбросил старый код и исправил его.

Он по существу работает, используя ключевое слово with и предоставляя ему замороженный пустой объект. Прототип пустого объекта заполняется свойствами null, ключи которого соответствуют именам глобальных переменных типа self, window и их перечисляемым ключам свойств; Объект-прототип также заморожен. eval затем вызывается внутри оператора with (почти так же, как скрипты выполняются с неявным блоком with(window){}, если я правильно понимаю). Когда вы пытаетесь получить доступ к window или его свойствам, вы перенаправляетесь (через блок with) в нулевые версии (с тем же ключом), которые находятся в пустом объекте (или, скорее, прототипе пустого объекта):

function buildQuarantinedEval(){
    var empty=(function(){
        var exceptionKeys = [
                "eval", "Object", //need exceptions for these else error. (ie, 'Exception: redefining eval is deprecated')
                "Number", "String", "Boolean", "RegExp", "JSON", "Date", "Array", "Math",
                "this",
                "strEval"
        ];
        var forbiddenKeys=["window","self"];
        var forbidden=Object.create(null);
        [window,this,self].forEach(function(obj){
            Object.getOwnPropertyNames(obj).forEach(function(key){
                forbidden[key]=null;
            });
            //just making sure we get everything
            Object.keys(obj).forEach(function(key){
                forbidden[key]=null;                    
            });
            for(var key in obj){
                forbidden[key]=null;
            }
        });
        forbiddenKeys.forEach(function(key){
            forbidden[key]=null;
        });
        exceptionKeys.forEach(function(key){
            delete forbidden[key];
        });
        Object.freeze(forbidden);
        var empty=Object.create(forbidden);
        Object.freeze(empty);
        return empty;
    })();
    return function(strEval){
        return (function(empty,strEval){
            try{
                with(empty){
                    return eval(strEval);
                }               
            }
            catch(err){
                return err.message;
            }
        }).call(empty,empty,strEval);
    };
}

Настройка путем создания функции/закрытия, которая оценивает некоторое выражение:

var qeval=buildQuarantinedEval();
qeval("'some expression'");     //evaluate

Тесты:

var testBattery=[
    "'abc'","8*8","console","window","location","XMLHttpRequest",
    "console","eval('1+1+1')","eval('7/9+1')","Date.now()","document",
    "/^http:/","JSON.stringify({a:0,b:1,c:2})","HTMLElement","typeof(window)",
    "Object.keys(window)","Object.getOwnPropertyNames(window)",
    "var result; try{result=window.location.href;}catch(err){result=err.message;}; result;",
    "parseInt('z')","Math.random()",
    "[1,2,3,4,8].reduce(function(p,c){return p+c;},0);"
];
var qeval=buildQuarantinedEval();
testBattery.map(function(code){
    const pad="                  ";
    var result= qeval(code);
    if(typeof(result)=="undefined")result= "undefined";
    if(result===null)result= "null";
    return (code+pad).slice(0,16)+": \t"+result;
}).join("\n");

Результаты:

/*
'abc'           :   abc
8*8             :   64
console         :   null
window          :   null
location        :   null
XMLHttpRequest  :   null
console         :   null
eval('1+1+1')   :   3
eval('7/9+1')   :   1.7777777777777777
Date.now()      :   1415335338588
document        :   null
/^http:/        :   /^http:/
JSON.stringify({:   {"a":0,"b":1,"c":2}
HTMLElement     :   null
typeof(window)  :   object
Object.keys(wind:   window is not an object
Object.getOwnPro:   can't convert null to object
var result; try{:   window is null
parseInt('z')   :   parseInt is not a function
Math.random()   :   0.8405481658901747
[1,2,3,4,8].redu:   18
*/

Примечания. Этот метод может завершиться неудачей, если некоторые свойства окна определены позже (после инициализации/создания нашей карантинной функции eval). Раньше я заметил, что некоторые ключи свойств не перечислены до тех пор, пока вы не получите доступ к свойству, после чего Object.keys или Object.getOwnPropertyNames, наконец, смогут захватить их ключи. С другой стороны, эта техника также может быть довольно агрессивной в блокировании объектов/функций, которые вы не хотите заблокировать (пример будет похож на parseInt); В этих случаях вам нужно вручную добавить глобальные объекты/функции, которые вы хотите в массив exceptionKeys.

* edit * Дополнительные соображения: насколько хорошо все это выполняется, полностью зависит от того, насколько хорошо маска соответствует свойствам ключей свойств объекта window. Каждый раз, когда вы добавляете элемент в документ и указываете ему новый идентификатор, вы просто вставляете новое свойство в объект глобального окна, что потенциально позволяет нашему "атакующему" захватить его и выйти из установленного нами карантина/брандмауэра ( т.е. access element.querySelector, а затем в конечном итоге окно obj). Таким образом, маска (т.е. Запрещенная переменная) либо должна постоянно обновляться с помощью метода часов, либо перестраиваться каждый раз; Первый конфликтует с необходимостью маскировать замороженный интерфейс, и последний из них дорог, чтобы перечислить все ключи окна для каждой оценки.

Как я уже говорил ранее, это в основном старый код, над которым я работал, а затем оставленный, который был быстро исправлен в короткие сроки. Так что это никоим образом не проверено полностью. Я оставлю это вам.


и jsfiddle


Ответ 5

Я уже говорил об этом в своем вопросе, но, чтобы сделать его более ясным, я также опубликую его в качестве ответа:

Я думаю, что принятый ответ на этот вопрос является правильным и единственным способом полностью изолировать и ограничить eval().

Он также защищен от этих хаков:

(new ('hello'.constructor.constructor)('alert("hello from global");'))()

(function(){return this;})().alert("hello again from global!");

while(true){} // if no worker --> R.I.P. browser tab

Array(5000000000).join("adasdadadasd") // memory --> boom!