Когда eval () в JavaScript не является злом?

Я пишу некоторый код JavaScript для анализа введенных пользователем функций (для работы с таблицами). Разобрав формулу, я смог преобразовать ее в JavaScript и запустить eval() на нем, чтобы дать результат.

Тем не менее, я всегда уклонялся от использования eval() если я могу избежать этого, потому что это зло (и, правильно или неправильно, я всегда думал, что это еще больше зла в JavaScript, потому что код, который нужно оценить, может быть изменен пользователем).

Итак, когда можно использовать его?

28 ответов

Решение

Я хотел бы воспользоваться моментом, чтобы рассмотреть предпосылку вашего вопроса - это eval() "зло". Слово "зло", используемое людьми на языке программирования, обычно означает "опасный", или, точнее, "способный причинить много вреда простой командой". Итак, когда можно использовать что-то опасное? Когда вы знаете, в чем опасность, и когда вы принимаете соответствующие меры предосторожности.

Кстати, давайте посмотрим на опасности при использовании eval(). Вероятно, существует много небольших скрытых опасностей, как и все остальное, но два больших риска - причина, по которой eval() считается злом, - это производительность и внедрение кода.

  • Производительность - eval() запускает интерпретатор / компилятор. Если ваш код скомпилирован, то это большой успех, потому что вам нужно вызвать, возможно, тяжелый компилятор во время выполнения. Тем не менее, JavaScript по-прежнему в основном интерпретируемый язык, что означает, что вызов eval() не является большим ударом по производительности в общем случае (но см. Мои конкретные замечания ниже).
  • Внедрение кода - eval() потенциально запускает строку кода с повышенными привилегиями. Например, программа, работающая как администратор /root, никогда не захочет eval() ввода пользователя, потому что этот ввод может быть "rm -rf /etc/ важный-файл" или хуже. Опять же, JavaScript в браузере не имеет этой проблемы, потому что программа все равно запускается под собственной учетной записью пользователя. Серверный JavaScript может иметь эту проблему.

На ваш конкретный случай. Насколько я понимаю, вы сами генерируете строки, поэтому, если вы будете осторожны, чтобы не генерировать такую ​​строку, как "rm -rf что-то важное", нет риска внедрения кода (но, пожалуйста, помните, это очень Трудно обеспечить это в общем случае). Кроме того, если вы работаете в браузере, то, как мне кажется, внедрение кода представляет собой довольно незначительный риск.

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

eval() не зло Или, если это так, это зло так же, как отражение, файловый / сетевой ввод / вывод, многопоточность и IPC являются "злыми" в других языках.

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

Когда вы доверяете источнику.

В случае JSON более или менее сложно вмешаться в источник, потому что он исходит от веб-сервера, которым вы управляете. Пока сам JSON не содержит данных, загруженных пользователем, нет существенного недостатка в использовании eval.

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

Давайте настоящие люди:

  1. Каждый крупный браузер теперь имеет встроенную консоль, которую ваш потенциальный хакер может обильно использовать для вызова любой функции с любым значением - почему они потрудились бы использовать оператор eval - даже если бы могли?

  2. Если для компиляции 2000 строк JavaScript требуется 0,2 секунды, в чем заключается мое снижение производительности, если я использую четыре строки JSON?

Даже объяснение Крокфорда "зло - это зло" слабо.

eval is Evil, функция eval является наиболее часто используемой функцией JavaScript. Избегай это

Как мог бы сам Крокфорд сказать: "Подобные высказывания имеют тенденцию вызывать иррациональный невроз. Не покупайте его".

Понимание eval и знание того, когда это может быть полезно, гораздо важнее. Например, eval - это разумный инструмент для оценки ответов сервера, сгенерированных вашим программным обеспечением.

Кстати, Prototype.js вызывает eval напрямую пять раз (в том числе в evalJSON() и evalResponse()). jQuery использует его в parseJSON (через конструктор функций).

Я склонен следовать советам Крокфорда eval()и избегать этого вообще. Даже способы, которые, кажется, требуют этого, не делают. Например, setTimeout() позволяет передавать функцию, а не eval.

setTimeout(function() {
  alert('hi');
}, 1000);

Даже если это надежный источник, я не использую его, потому что код, возвращаемый JSON, может быть искажен, что в лучшем случае может сделать что-то нехорошее, а в худшем - показать что-то плохое.

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

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

Производительность EVAL может быть увеличена с помощью следующего метода; вместо выполнения скрипта вы должны вернуть функцию.

var a = eval("3 + 5");

Это должно быть организовано как

var f = eval("(function(a,b) { return a + b; })");

var a = f(3,5);

Кеширование f, безусловно, улучшит скорость.

Также Chrome позволяет легко отлаживать такие функции.

Что касается безопасности, использование eval или нет вряд ли будет иметь значение,

  1. Прежде всего, браузер вызывает весь сценарий в песочнице.
  2. Любой код, который является злом в EVAL, является злом в самом браузере. Злоумышленник или кто-либо другой может легко внедрить узел сценария в DOM и сделать что-нибудь, если он / она сможет что-либо проверить. Не использовать EVAL не будет иметь никакого значения.
  3. В основном это плохая защита на стороне сервера, которая вредна. Плохая проверка файлов cookie или плохая реализация ACL на сервере вызывают большинство атак.
  4. Недавно в нативном коде Java была уязвимость Java и т. Д. JavaScript был и предназначен для работы в песочнице, тогда как апплеты были разработаны для работы вне песочницы с сертификатами и т. Д., Которые приводят к уязвимостям и многим другим.
  5. Написание кода для имитации браузера не сложно. Все, что вам нужно сделать, это сделать HTTP-запрос к серверу с вашей любимой строкой агента пользователя. Все инструменты тестирования в любом случае имитируют браузеры; если злоумышленник хочет причинить вам вред, EVAL является их последним средством. У них есть много других способов справиться с вашей безопасностью на стороне сервера.
  6. DOM браузера не имеет доступа к файлам и не имеет имени пользователя. Фактически ничто на машине, к которой eval может дать доступ.

Если ваша серверная безопасность достаточно надежна, чтобы кто-то мог атаковать откуда угодно, вам не стоит беспокоиться о EVAL. Как я уже упоминал, если EVAL не существует, у злоумышленников есть много инструментов для взлома вашего сервера, независимо от возможностей EVAL вашего браузера.

Eval хорош только для генерации некоторых шаблонов для выполнения сложной обработки строк на основе чего-то, что не используется заранее. Например, я предпочту

"FirstName + ' ' + LastName"

В отличие от

"LastName + ' ' + FirstName"

Как мое отображаемое имя, которое может исходить из базы данных и не является жестко закодированным.

Нижняя линия

Если вы создали или санировали код, который вы eval Это никогда не зло.

Чуть подробнее

eval Это плохо, если работает на сервере с использованием входных данных, предоставленных клиентом, которые не были созданы разработчиком или не были обработаны разработчиком.

eval не вредно, если работает на клиенте, даже если используется неантифицированный ввод, созданный клиентом.

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

аргументация

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

При отладке в Chrome (v28.0.1500.72) я обнаружил, что переменные не связаны с замыканиями, если они не используются во вложенной функции, которая создает замыкание. Я думаю, это оптимизация движка JavaScript.

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

Вот мой тестовый код:

(function () {
    var eval = function (arg) {
    };

    function evalTest() {
        var used = "used";
        var unused = "not used";

        (function () {
            used.toString();   // Variable "unused" is visible in debugger
            eval("1");
        })();
    }

    evalTest();
})();

(function () {
    var eval = function (arg) {
    };

    function evalTest() {
        var used = "used";
        var unused = "not used";

        (function () {
            used.toString();   // Variable "unused" is NOT visible in debugger
            var noval = eval;
            noval("1");
        })();
    }

    evalTest();
})();

(function () {
    var noval = function (arg) {
    };

    function evalTest() {
        var used = "used";
        var unused = "not used";

        (function () {
            used.toString();    // Variable "unused" is NOT visible in debugger
            noval("1");
        })();
    }

    evalTest();
})();

Здесь я хотел бы отметить, что eval() не обязательно должен ссылаться на нативный eval() функция. Все зависит от названия функции. Так что при звонке родной eval() с псевдонимом (скажем, var noval = eval; а затем во внутренней функции noval(expression);) тогда оценка expression может потерпеть неудачу, когда ссылается на переменные, которые должны быть частью замыкания, но на самом деле это не так.

Я видел, как люди выступают за то, чтобы не использовать eval, потому что это зло, но я видел, как те же люди динамически используют Function и setTimeout, поэтому они используют eval под капотами:D

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

Теперь эпилог к ​​этому посту:

Если вам ДЕЙСТВИТЕЛЬНО это нужно (80% времени eval НЕ требуется) и вы уверены в том, что делаете, просто используйте eval (или лучшую функцию;)), замыкания и ООП покрывают 80/90% случай, когда eval может быть заменен с использованием другой логики, остальное - это динамически генерируемый код (например, если вы пишете интерпретатор) и, как вы уже сказали, оценка JSON (здесь вы можете использовать безопасную оценку Крокфорда;))

Microsoft объясняет, почему eval() работает медленно в их браузере, в блоге IE, рекомендации по производительности IE+JavaScript, часть 2. Неэффективность кода JavaScript.

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

... И 9 раз из 10 вы можете легко избежать этого путем рефакторинга.

Eval - это не зло, им просто злоупотребляют.

Если вы создали входящий в него код или можете ему доверять, все в порядке. Люди продолжают говорить о том, что ввод пользователя не имеет значения для eval. Ну вроде ~

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

В зависимости от того, где находится eval, многие веб-сайты используют SPA, и eval может упростить пользователю доступ к внутренним компонентам приложения, что в противном случае было бы нелегко. Теперь они могут создать фальшивое расширение для браузера, которое сможет записать этот eval и снова украсть данные.

Просто нужно понять, какой смысл использовать eval. Генерация кода не совсем идеальна, если вы можете просто создавать методы для подобных вещей, использовать объекты и т.п.

А теперь хороший пример использования eval. Ваш сервер читает файл swagger, который вы создали. Многие параметры URL создаются в формате{myParam}. Итак, вы хотите прочитать URL-адреса, а затем преобразовать их в строки шаблона без необходимости выполнять сложные замены, потому что у вас много конечных точек. Так что вы можете сделать что-то вроде этого. Обратите внимание, это очень простой пример.

const params = { id: 5 };

const route = '/api/user/{id}';
route.replace(/{/g, '${params.');

// use eval(route); to do something

Это нормально, если вы имеете полный контроль над кодом, который передается eval функция.

На стороне сервера eval полезен при работе с внешними сценариями, такими как sql или influenxdb или mongo. Там, где пользовательская проверка во время выполнения может быть выполнена без повторного развертывания ваших служб.

Например, служба достижений со следующими метаданными

{
  "568ff113-abcd-f123-84c5-871fe2007cf0": {
    "msg_enum": "quest/registration",
    "timely": "all_times",
    "scope": [
      "quest/daily-active"
    ],
    "query": "`SELECT COUNT(point) AS valid from \"${userId}/dump/quest/daily-active\" LIMIT 1`",
    "validator": "valid > 0",
    "reward_external": "ewallet",
    "reward_external_payload": "`{\"token\": \"${token}\", \"userId\": \"${userId}\", \"amountIn\": 1, \"conversionType\": \"quest/registration:silver\", \"exchangeProvider\":\"provider/achievement\",\"exchangeType\":\"payment/quest/registration\"}`"
  },
  "efdfb506-1234-abcd-9d4a-7d624c564332": {
    "msg_enum": "quest/daily-active",
    "timely": "daily",
    "scope": [
      "quest/daily-active"
    ],
    "query": "`SELECT COUNT(point) AS valid from \"${userId}/dump/quest/daily-active\" WHERE time >= '${today}' ${ENV.DAILY_OFFSET} LIMIT 1`",
    "validator": "valid > 0",
    "reward_external": "ewallet",
    "reward_external_payload": "`{\"token\": \"${token}\", \"userId\": \"${userId}\", \"amountIn\": 1, \"conversionType\": \"quest/daily-active:silver\", \"exchangeProvider\":\"provider/achievement\",\"exchangeType\":\"payment/quest/daily-active\"}`"
  }
}

Которые затем позволяют,

  • Прямая инъекция объекта / значений через буквальную строку в формате json, полезную для шаблонных текстов

  • Можно использовать в качестве компаратора, скажем, мы устанавливаем правила, как проверять квест или события в CMS

Против этого:

  • Могут быть ошибки в коде и поломка вещей в сервисе, если они не полностью протестированы.

  • Если хакер может написать скрипт в вашей системе, то вы в значительной степени облажались.

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

Что касается клиентского сценария, я думаю, что вопрос безопасности является спорным вопросом. Все, что загружено в браузер, подвергается манипуляциям и должно рассматриваться как таковое. Использование оператора eval() практически исключает риск, когда есть гораздо более простые способы выполнения кода JavaScript и / или манипулирования объектами в DOM, например, строкой URL в вашем браузере.

javascript:alert("hello");

Если кто-то хочет манипулировать своим DOM, я говорю: откажитесь. Безопасность для предотвращения любого типа атаки всегда должна быть ответственностью серверного приложения, и точка.

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

<html>
    <body>
        <textarea id="output"></textarea><br/>
        <input type="text" id="input" />
        <button id="button" onclick="execute()">eval</button>

        <script type="text/javascript">
            var execute = function(){
                var inputEl = document.getElementById('input');
                var toEval = inputEl.value;
                var outputEl = document.getElementById('output');
                var output = "";

                try {
                    output = eval(toEval);
                }
                catch(err){
                    for(var key in err){
                        output += key + ": " + err[key] + "\r\n";
                    }
                }
                outputEl.value = output;
            }
        </script>
    <body>
</html>

eval редко является правильным выбором. Несмотря на то, что могут быть многочисленные случаи, когда вы можете выполнить то, что вам нужно, объединяя сценарий и выполняя его на лету, в вашем распоряжении обычно гораздо более мощные и поддерживаемые методы: нотация ассоциативного массива (obj["prop"] такой же как obj.prop), замыкания, объектно-ориентированные методы, функциональные методы - используйте их вместо этого.

Мой пример использования eval: импорт.

Как это обычно делается.

var components = require('components');
var Button = components.Button;
var ComboBox = components.ComboBox;
var CheckBox = components.CheckBox;
...
// That quickly gets very boring

Но с помощью eval и небольшая вспомогательная функция выглядит намного лучше:

var components = require('components');
eval(importable('components', 'Button', 'ComboBox', 'CheckBox', ...));

importable может выглядеть так (эта версия не поддерживает импорт конкретных элементов).

function importable(path) {
    var name;
    var pkg = eval(path);
    var result = '\n';

    for (name in pkg) {
        result += 'if (name !== undefined) throw "import error: name already exists";\n'.replace(/name/g, name);
    }

    for (name in pkg) {
        result += 'var name = path.name;\n'.replace(/name/g, name).replace('path', path);
    }
    return result;
}

Поскольку никто еще не упомянул об этом, позвольте мне добавить, что evalочень полезен для взаимодействия Webassembly-Javascript. Хотя, безусловно, идеально иметь готовые сценарии, включенные в вашу страницу, которые ваш код WASM может вызывать напрямую, иногда это невозможно, и вам нужно передать динамический Javascript из языка Webassembly, такого как C#, чтобы действительно выполнить то, что вам нужно сделать.

Это также безопасно в этом сценарии, потому что у вас есть полный контроль над тем, что передается. Ну, я должен сказать, что это не менее безопасно, чем составление операторов SQL с использованием C#, то есть это нужно делать осторожно (правильно экранировать строки и т. д.). .) всякий раз, когда для создания скрипта используются предоставленные пользователем данные. Но с этой оговоркой он имеет четкое место в ситуациях взаимодействия и далек от «зла».

Когда eval() в JavaScript не является злом?

Я всегда пытаюсь отговорить от использования eval. Почти всегда доступно более чистое и удобное решение. Eval не нужен даже для анализа JSON. Эвал добавляет в ад обслуживания. Недаром его осуждают такие мастера, как Дуглас Крокфорд.

Но я нашел один пример, где его следует использовать:

Когда вам нужно передать выражение.

Например, у меня есть функция, которая создает общий google.maps.ImageMapType объект для меня, но я должен сказать ему рецепт, как он должен создать URL-адрес плитки из zoom а также coord параметры:

my_func({
    name: "OSM",
    tileURLexpr: '"http://tile.openstreetmap.org/"+b+"/"+a.x+"/"+a.y+".png"',
    ...
});

function my_func(opts)
{
    return new google.maps.ImageMapType({
        getTileUrl: function (coord, zoom) {
            var b = zoom;
            var a = coord;
            return eval(opts.tileURLexpr);
        },
        ....
    });
}

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

Вопросы безопасности являются наиболее известными. Но также имейте в виду, что JavaScript использует JIT-компиляцию, и это плохо работает с eval. Eval чем-то напоминает "черный ящик" для компилятора, и JavaScript должен уметь предсказывать код заранее (до некоторой степени), чтобы безопасно и правильно применять оптимизацию производительности и область видимости. В некоторых случаях влияние на производительность может даже повлиять на другой код за пределами eval.

Если вы хотите узнать больше: https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/ch2.md#eval

Если это действительно нужно, eval - это не зло. Но 99,9% случаев использования eval, с которыми я сталкиваюсь, не нужны (не считая setTimeout).

Для меня зло - это не производительность или даже проблема безопасности (ну, косвенно, это и то и другое). Все такие ненужные использования eval добавляют к адскому обслуживанию. Инструменты рефакторинга скинуты. Искать код сложно. Непредвиденные последствия этих уловок легион.

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

Итак... когда не следует использовать eval()? Eval() не должен использоваться, только когда есть вероятность, что третье лицо может изменить его. Например, перехватить соединение между клиентом и вашим сервером (но если это проблема, используйте HTTPS). Вы не должны использовать eval() для анализа кода, написанного другими людьми, как на форуме.

Хотя может быть множество случаев, когда вы можете выполнить то, что вам нужно, объединив сценарий вместе и запустив его на лету, в вашем распоряжении обычно есть гораздо более мощные и удобные в сопровождении методы. eval редко бывает правильным выбором: нотация ассоциативного массива (obj["prop"] совпадает с obj.prop), замыкания, объектно-ориентированные методы, функциональные методы - используйте их вместо этого.

Генерация кода. Недавно я написал библиотеку под названием Hyperbars, которая ликвидирует разрыв между virtual-dom и рулем. Это делается путем анализа шаблона руля и преобразования его в гиперскрипт. Гиперскрипт сначала генерируется в виде строки, а затем возвращается, eval() это превратить его в исполняемый код. я нашел eval() в этой конкретной ситуации полная противоположность зла.

В основном из

<div>
    {{#each names}}
        <span>{{this}}</span>
    {{/each}}
</div>

К этому

(function (state) {
    var Runtime = Hyperbars.Runtime;
    var context = state;
    return h('div', {}, [Runtime.each(context['names'], context, function (context, parent, options) {
        return [h('span', {}, [options['@index'], context])]
    })])
}.bind({}))

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

Вы можете увидеть, как была достигнута генерация кода, если вам интересно.

Я считаю, что eval - очень мощная функция для клиентских веб-приложений и безопасная... Такая же безопасная, как и JavaScript, а это не так.:-) Проблемы безопасности, по сути, являются проблемой на стороне сервера, потому что теперь, с помощью такого инструмента, как Firebug, вы можете атаковать любое приложение JavaScript.

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

Eval полезен для генерации кода, когда у вас нет макросов.

Для (глупого) примера, если вы пишете компилятор Brainfuck, вы, вероятно, захотите создать функцию, которая выполняет последовательность инструкций в виде строки, и вычислить ее для возврата функции.

Когда вы анализируете структуру JSON с помощью функции синтаксического анализа (например, jQuery.parseJSON), она ожидает идеальную структуру файла JSON (каждое имя свойства в двойных кавычках). Тем не менее, JavaScript является более гибким. Поэтому вы можете использовать eval(), чтобы избежать этого.

Другие вопросы по тегам