Чому eval небезпечний
eval() це вбудована функція, яка бере рядок і змушує рушій JavaScript виконати його як звичайний код. Вона одна з найнебезпечніших і найсуперечливіших функцій мови, бо стирає межу між даними і кодом, тому її майже завжди варто замінити безпечнішою конструкцією.
Теорія
TL;DR
eval(str)виконує вміст рядкаstrяк JavaScript-код і повертає значення останнього виразу.- Головна проблема це безпека: неконтрольований рядок означає виконання довільного коду, XSS та ін'єкції.
- Продуктивність падає, бо рушій не може заздалегідь проаналізувати й оптимізувати такий код (JIT блокується).
eval()працює в поточній області видимості, тому може створювати і перезаписувати змінні навколо себе.- Для майже всіх реальних задач є заміна:
JSON.parse(), доступ за ключемobj[prop], мапа функцій,Function()у контрольованому випадку.
Швидкий приклад
eval("console.log('Hello from eval!')");
// output: Hello from eval!
const x = 10;
const y = 5;
console.log(eval('x + y')); // 15eval «розуміє», що рядок 'x + y' це вираз, обчислює його в поточній області видимості і повертає результат.
Що саме робить eval
eval() не «парсить формулу» і не має власної пісочниці: він передає рядок тому самому рушієві, який виконує решту вашого коду, і з тими самими правами.
Приклад 1, виконання виразу:
const x = 10;
const y = 5;
console.log(eval('x + y')); // 15Приклад 2, створення змінних (небезпечно):
eval('var z = 100;');
console.log(z); // 100Проблема в тому, що змінна z створюється в поточній області видимості і може перезаписати наявні значення.
Приклад 3, динамічний код (погана практика):
function runCode(code) {
eval(code);
}
runCode("alert('running arbitrary code')");Це виконає будь-який код, переданий рядком. Якщо цей рядок прийшов від користувача, ви отримуєте XSS-атаку або повний контроль над застосунком.
Небезпека для безпеки (XSS та ін'єкції)
eval() виконає будь-який переданий код, зокрема шкідливий, введений користувачем або зловмисником.
const userInput = "alert('pwned')";
eval(userInput); // runs straight away in the browserЗамість alert там може бути код, який читає document.cookie, localStorage чи токен сесії і надсилає їх на сторонній сервер. Це прямий шлях до XSS-атак і витоку даних. Саме тому Content Security Policy за замовчуванням блокує eval() директивою unsafe-eval.
Зниження продуктивності
Код усередині eval() виконується повільніше, бо рушій не може його оптимізувати: він не знає заздалегідь, що там буде. Кожен виклик eval() змушує рушій наново аналізувати та інтерпретувати рядок, а функція, що містить eval(), часто втрачає оптимізації JIT цілком, бо рушій не може довести, які змінні в ній використовуються.
Ламає область видимості
Код усередині eval() може створювати або змінювати змінні поточного контексту, що робить програму непередбачуваною і складною для налагодження.
let a = 10;
eval('a = 99;');
console.log(a); // 99, the value changed "from the inside"У строгому режимі ('use strict') ситуація трохи краща: eval() отримує власну область видимості для оголошень, тому var усередині нього більше не тече назовні. Але читати й перезаписувати наявні змінні він усе одно може.
Погана читабельність і підтримка
eval() робить код «магічним»: до моменту виконання невідомо, що саме буде запущено. Лінтери й статичний аналіз не бачать усередині рядка нічого, автодоповнення й перейменування символів у редакторі не працюють, а стектрейси вказують на згенерований код, а не на зрозуміле місце у файлі.
Безпечні альтернативи
| Задача | Чим замінити |
|---|---|
| Виконати математичний вираз | Function() або готовий парсер виразів |
| Розібрати JSON | JSON.parse() |
| Виконати динамічний код із заздалегідь відомих функцій | Об'єкт з мапою функцій |
| Отримати властивість за іменем | obj[propName], а не eval('obj.' + propName) |
Приклади заміни.
Погано:
eval("user.name = 'Tim'");Добре:
user['name'] = 'Tim';Погано:
const obj = eval('(' + jsonStr + ')');Добре:
const obj = JSON.parse(jsonStr);Погано:
eval('sum(5, 10)');Добре:
const actions = { sum: (a, b) => a + b };
actions['sum'](5, 10);Типові помилки
- Вважати, що
new Function(...)безпечний. Він теж компілює рядок у код, просто в глобальній області видимості, тому для недовіреного вводу він так само небезпечний. - Використовувати
eval()для розбору JSON. Це і повільніше, і вразливо;JSON.parse()існує саме для цього. - Будувати ім'я властивості рядком:
eval('obj.' + key)замістьobj[key]. Другий варіант коротший, швидший і безпечний. - Думати, що санітизація рядка робить
eval()безпечним. Надійно екранувати довільний JavaScript практично неможливо. - Забувати про приховані форми
eval: рядок як перший аргументsetTimeout('doWork()', 100)виконується так само. - Дивуватися, чому
eval()не працює в продакшені: CSP безunsafe-evalблокує його, і це нормальна поведінка.
Коротко запам'ятати
| Аспект | Оцінка |
|---|---|
| Що робить | Виконує рядок як JavaScript-код |
| Безпека | Дуже небезпечно: XSS та ін'єкції |
| Продуктивність | Повільно |
| Налагодження | Складно |
| Оптимізація | Блокує JIT |
| Коли використовувати | Лише в дуже рідких випадках, коли рядок повністю під вашим контролем |
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.