Eval (от англ. evaluate — вычислить) — встроенная функция или конструкция языка, которая берет строку и выполняет ее как программный код. Строка "2 + 3 * 4" для eval — не текст, а выражение: результат будет 14. Eval есть в JavaScript, Python, PHP, Ruby и многих других языках.
Содержание
Главное, что нужно знать про eval: если в строку может попасть ввод пользователя, он получает возможность выполнить свой код в вашей программе. Поэтому ниже — как eval работает в JavaScript и Python, как выглядит атака и какие безопасные замены закрывают типовые задачи. Все примеры проверены на Node.js 24.21 и Python 3.14.7 (сентябрь 2026).
Не путайте с «evals» в машинном обучении: там это сокращение от evaluation, наборы тестов для оценки моделей. К функции eval отношения не имеет.
Три понятия, которые путают
| Термин | Что это | Пример |
|---|---|---|
| Выражение (expression) | Код, который дает значение | 2 + 3, x * 2 |
| Инструкция (statement) | Код, который выполняет действие | y = 1, for, import |
| Разбор данных (parsing) | Превращение текста в данные без выполнения | JSON.parse, json.loads |
В Python eval принимает только выражение, а для инструкций есть exec. В JavaScript eval принимает любой код. Безопасные альтернативы относятся к третьей строке: они читают данные, но ничего не выполняют.
Eval в JavaScript
Минимальный пример и важная деталь — область видимости:
console.log(eval("2 + 3 * 4"));
function direct() {
const secret = 42;
return eval("secret"); // прямой вызов: видит локальные переменные
}
function indirect() {
const secret = 42;
const geval = eval;
try {
return geval("secret"); // косвенный вызов: только глобальная область
} catch (e) {
return e.name + ": " + e.message;
}
}
console.log(direct());
console.log(indirect());
14
42
ReferenceError: secret is not defined
Прямой eval (вызов по имени eval(...)) выполняет код в текущей области и видит все локальные переменные функции. Косвенный eval (через другую ссылку, например const geval = eval) работает в глобальной области. Отсюда первый риск: строка внутри прямого eval может прочитать и изменить все, что видит окружающая функция.
Второе отличие — строгий режим. Без "use strict" строка eval("var inside = 1") создает переменную снаружи, и typeof inside дает number. В строгом режиме у eval своя область, и тот же код дает undefined. В ES-модулях строгий режим включен всегда.
Eval в Python
x = 10
print(eval("2 + 3 * 4"))
print(eval("x * 2"))
print(eval("x * 2", {"x": 5}))
try:
eval("y = 1")
except SyntaxError as e:
print(type(e).__name__, e.msg)
exec("y = 1")
print(y)
14
20
10
SyntaxError invalid syntax
1
Сигнатура: eval(expression, globals=None, locals=None). Без словарей выражение видит переменные места вызова (x = 10 -> 20). Со словарем {"x": 5} видит только его содержимое плюс встроенные функции: если ключа __builtins__ в словаре нет, Python добавляет его сам. Присваивание y = 1 — инструкция, поэтому eval отвечает SyntaxError, а exec выполняет.
Почему eval опасен: инъекция кода
Типичная ошибка новичка — калькулятор, который передает ввод прямо в eval. На JavaScript (Node.js, CommonJS-скрипт):
function calc(input) {
return eval(input); // так делать нельзя
}
console.log(calc("2 + 2"));
console.log(calc("process.version"));
console.log(typeof calc("require('fs').readdirSync"));
4
v24.21.0
function
Вместо арифметики «калькулятор» выдал версию Node.js и доступ к модулю файловой системы. Злоумышленник подставит не безобидное чтение версии, а чтение файлов, переменных окружения с паролями или запуск команд ОС. В браузере то же самое превращается в XSS: чужой код выполняется от имени пользователя на вашем сайте.
В Python картина та же:
expr = "__import__('platform').python_version()"
print(eval(expr))
print(eval("().__class__.__base__.__subclasses__()[0]", {"__builtins__": {}}))
3.14.7
<class 'type'>
Первая строка импортировала модуль прямо из выражения. Вторая показывает, почему пустой __builtins__ — не песочница: через атрибуты любого объекта выражение добирается до внутренних классов интерпретатора, а от них известными цепочками — до опасных функций. Урезанные globals мешают случайным ошибкам, но не защищают от целенаправленной атаки.
Главная причина отказа от eval — именно выполнение чужого кода. Второстепенные минусы: код в строке не видят линтеры и подсветка, его тяжело отлаживать, а прямой eval в JavaScript мешает движку оптимизировать функцию.
Безопасные альтернативы
Сначала сформулируйте, какую задачу решал eval. Почти всегда это одна из трех:
| Задача | Вместо eval | Что делает с кодом во входе |
|---|---|---|
| Прочитать JSON | JSON.parse (JS), json.loads (Python) |
Отклоняет с ошибкой |
| Прочитать литерал Python (словарь, список, кортеж) | ast.literal_eval |
Отклоняет с ValueError (битый синтаксис — SyntaxError) |
| Посчитать выражение от пользователя | Разбор + таблица разрешенных операций | Отклоняет все, чего нет в таблице |
| Вызвать функцию по имени | Словарь имя -> функция |
Неизвестное имя = ошибка |
Разбор данных: JSON.parse и ast.literal_eval
const text = '{"name": "Otus", "tags": ["js", "python"], "active": true}';
const data = JSON.parse(text);
console.log(data.tags[1]);
try {
JSON.parse("{name: 'Otus'}");
} catch (e) {
console.log(e.name + ": " + e.message);
}
try {
JSON.parse('process.exit(1)');
} catch (e) {
console.log(e.name + ": " + e.message);
}
python
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
SyntaxError: Unexpected token 'p', "process.exit(1)" is not valid JSON
JSON.parse строже, чем литерал объекта JavaScript: ключи только в двойных кавычках, одинарные кавычки запрещены. Текст ошибки зависит от движка, здесь — V8 в Node.js 24.
import ast, json
print(ast.literal_eval("{'a': 1, 'b': [2, 3], 'c': (True, None)}"))
for s in ["__import__('os').getcwd()", "1 + 1", "2 ** 10"]:
try:
print(ast.literal_eval(s))
except ValueError as e:
print("ValueError:", e)
print(json.loads('{"a": 1, "b": [2, 3], "c": null}'))
{'a': 1, 'b': [2, 3], 'c': (True, None)}
ValueError: malformed node or string on line 1: Call(func=Attribute(value=Call(func=Name(...), args=[Constant(...)], keywords=[]), attr='getcwd', ctx=Load()), args=[], keywords=[])
ValueError: malformed node or string on line 1: BinOp(left=Constant(value=1, kind=None), op=Add(), right=Constant(value=1, kind=None))
ValueError: malformed node or string on line 1: BinOp(left=Constant(value=2, kind=None), op=Pow(), right=Constant(value=10, kind=None))
{'a': 1, 'b': [2, 3], 'c': None}
ast.literal_eval принимает только литералы: строки, числа, True/False/None, списки, кортежи, словари, множества. Даже 1 + 1 он не считает. Учтите, что ValueError — не единственное исключение: незакрытая скобка "(" дает SyntaxError, а "{[1]: 2}" — TypeError, поэтому на внешнем вводе ловите (ValueError, SyntaxError, TypeError, MemoryError, RecursionError). Граница: документация Python предупреждает, что очень большой или глубоко вложенный ввод может исчерпать память или стек, поэтому длину строки ограничивайте заранее. Для обмена данными между системами берите JSON, а literal_eval оставьте для конфигов и дампов в синтаксисе Python.
Калькулятор: таблица операций вместо eval
Если пользователю нужно вводить выражения, разберите строку и выполняйте только операции из явного списка. Базовый защищенный вариант на JavaScript для формата «число операция число»:
const OPS = {
"+": (a, b) => a + b,
"-": (a, b) => a - b,
"*": (a, b) => a * b,
"/": (a, b) => {
if (b === 0) throw new RangeError("деление на ноль");
return a / b;
},
};
function calc(input) {
if (input.length > 1000) throw new RangeError("слишком длинный ввод");
// Формат строго: число, пробелы, операция, пробелы, число
const m = /^\s*(-?\d+(?:\.\d+)?)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)\s*$/.exec(input);
if (!m) throw new SyntaxError("ожидается формат «число операция число»");
const [, a, op, b] = m;
const result = OPS[op](Number(a), Number(b));
if (!Number.isFinite(result)) throw new RangeError("результат вне диапазона");
return result;
}
for (const s of ["2 + 3", "7 / 2", "-4 * 2.5", "1 / 0", "process.exit(1)", "9".repeat(310) + " * 1"]) {
try {
console.log(`${s.slice(0, 16)} -> ${calc(s)}`);
} catch (e) {
console.log(`${s.slice(0, 16)} -> ${e.name}: ${e.message}`);
}
}
2 + 3 -> 5
7 / 2 -> 3.5
-4 * 2.5 -> -10
1 / 0 -> RangeError: деление на ноль
process.exit(1) -> SyntaxError: ожидается формат «число операция число»
9999999999999999 -> RangeError: результат вне диапазона
Регулярное выражение пропускает только четыре символа операции, поэтому ключ таблицы никогда не будет чем-то вроде constructor. Если ключ берется из ввода без такой проверки, используйте Map или Object.hasOwn(OPS, op): у обычного объекта есть унаследованные свойства.
Для выражений со скобками и приоритетом в Python удобно разобрать строку в дерево (AST) и обойти его, разрешая только нужные узлы:
import ast
import math
import operator
# Разрешенные операции: узел AST -> функция
BIN_OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
}
UNARY_OPS = {ast.USub: operator.neg, ast.UAdd: operator.pos}
MAX_LEN = 200 # длина выражения
MAX_DEPTH = 50 # вложенность узлов
def _eval(node, depth=0):
if depth > MAX_DEPTH:
raise ValueError("слишком глубокая вложенность")
if isinstance(node, ast.Constant) and type(node.value) in (int, float):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in BIN_OPS:
left = _eval(node.left, depth + 1)
right = _eval(node.right, depth + 1)
return BIN_OPS[type(node.op)](left, right)
if isinstance(node, ast.UnaryOp) and type(node.op) in UNARY_OPS:
return UNARY_OPS[type(node.op)](_eval(node.operand, depth + 1))
bad = getattr(node, "op", node) # для BinOp покажем саму операцию
raise ValueError(f"недопустимый элемент: {type(bad).__name__}")
def safe_calc(expr: str) -> float:
if len(expr) > MAX_LEN:
raise ValueError("слишком длинное выражение")
try:
tree = ast.parse(expr.strip(), mode="eval")
except (SyntaxError, RecursionError, MemoryError):
raise ValueError("не удалось разобрать выражение")
result = _eval(tree.body)
if not math.isfinite(result): # 9e999 или 1e308 * 10 дают inf
raise ValueError("результат вне диапазона")
return result
for s in ["2 + 3 * 4", "(1 + 2) / 4", "-5 - -2", "2 ** 10",
"__import__('os').getcwd()", "1 / 0", "1e308 * 10", "-" * 60 + "1", "9" * 300]:
try:
print(f"{s[:20]!r} -> {safe_calc(s)}")
except (ValueError, ZeroDivisionError) as e:
print(f"{s[:20]!r} -> ошибка: {e}")
'2 + 3 * 4' -> 14
'(1 + 2) / 4' -> 0.75
'-5 - -2' -> -3
'2 ** 10' -> ошибка: недопустимый элемент: Pow
"__import__('os').get" -> ошибка: недопустимый элемент: Call
'1 / 0' -> ошибка: division by zero
'1e308 * 10' -> ошибка: результат вне диапазона
'--------------------' -> ошибка: слишком глубокая вложенность
'99999999999999999999' -> ошибка: слишком длинное выражение
Здесь ast.parse только строит дерево и ничего не выполняет. Вызовы функций, атрибуты, имена переменных и возведение в степень в таблицу не входят, поэтому отклоняются. Степень исключена намеренно: 9 ** 9 ** 9 даже без инъекции надолго займет процессор и память. Лимиты длины и глубины отсекают ввод, рассчитанный на переполнение стека; цена — длинная плоская сумма вроде ста слагаемых 1+1+... тоже упрется в лимит глубины, потому что дерево у нее левоглубокое. Проверка math.isfinite держит тот же контракт, что и JavaScript-версия: бесконечность не выдается за ответ.
Что еще нужно в production
Калькуляторы выше — базовые примеры, а не полная защита сервиса. В рабочем коде добавьте:
- Запрет eval на уровне инструментов: правила ESLint
no-eval,no-implied-eval,no-new-func; в Python — проверки Bandit или Ruff на вызовыeval/exec. - Помните про скрытые формы eval:
new Function("...")в JavaScript, а в браузере ещеsetTimeout("строка")иsetInterval("строка")(Node.js 24 строку в таймер не принимает и бросаетTypeError);execиcompileв Python. - В браузере — Content Security Policy с директивой
script-src(илиdefault-src) без'unsafe-eval': тогда eval,new Functionи строковые таймеры блокируются даже при ошибке в коде. Без самой директивы запрета нет. - В Node.js флаг
--disallow-code-generation-from-stringsзапрещает eval во всем процессе. Проверка на Node.js 24:eval("1 + 1")падает сEvalError: Code generation from strings disallowed for this context. - Не считайте песочницей модуль
node:vmили урезанные globals в Python: документация Node.js прямо говорит, чтоvm— не механизм безопасности. Если чужой код действительно нужно выполнять, запускайте его в отдельном процессе или контейнере с ограничением времени, памяти, сети и прав. - Ограничения по объему и частоте запросов, журналирование отклоненного ввода и тесты со злым вводом: вызовы, глубокая вложенность, очень длинные числа.
Eval в других языках
| Язык | Как называется | Особенность |
|---|---|---|
| JavaScript | eval(), new Function() |
Прямой вызов видит локальные переменные |
| Python | eval() и exec() |
eval — только выражения, exec — инструкции |
| PHP | конструкция eval() |
Код без тега <?php; синтаксическая ошибка бросает ParseError (с PHP 7) |
Правило для всех языков одно: eval оправдан в инструментах разработчика (REPL, отладчик, генератор кода по доверенному шаблону), но не там, где в строку может попасть внешний ввод.
Выводы
- Eval выполняет строку как код: в Python только выражение, в JavaScript — любой код, причем прямой вызов видит локальные переменные.
- Если в строку попадает ввод пользователя, это инъекция кода: чтение файлов, секретов, запуск команд.
- Урезанные globals в Python и
node:vmне делают eval безопасным. - Для данных берите
JSON.parse,json.loadsиast.literal_eval, для выражений — разбор и таблицу разрешенных операций с лимитами. - Закрепите запрет линтером, CSP или флагом Node.js, чтобы eval не вернулся в код незаметно.
Где применяется / связь с практикой
Освойте тему на практике
Разбор данных вместо выполнения строк, области видимости, строгий режим и работа с JSON — базовые темы, которые разбирают на курсе «JavaScript-разработчик. Базовый уровень». Там же отрабатывают привычку проверять пользовательский ввод до того, как он попадет в логику программы.
Освойте тему на практике
Посмотреть разборы кода вживую и задать вопросы преподавателям можно на бесплатных открытых уроках Otus.
FAQ
Можно ли безопасно передавать в eval только числа?
Если строка действительно состоит из цифр, проще и надежнее Number(s) в JavaScript или int(s)/float(s) в Python. Проверка «только числа» перед eval обычно дырявая, а сам eval после нее не нужен.
Чем new Function отличается от eval?
new Function("a", "b", "return a + b") тоже компилирует код из строки, но тело функции видит только глобальную область, а не локальные переменные места вызова. От инъекции это не спасает, поэтому правила те же.
Есть ли у eval легитимные применения?
Да: интерактивные консоли (REPL), отладчики, инструменты, которые генерируют код из доверенных шаблонов на этапе сборки. Общее у них одно — источник строки контролирует сам разработчик.



