Eval в программировании: что делает, чем опасен и чем заменить

Eval в программировании: что делает, чем опасен и чем заменить Полезное

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

Калькуляторы выше — базовые примеры, а не полная защита сервиса. В рабочем коде добавьте:

  1. Запрет eval на уровне инструментов: правила ESLint no-eval, no-implied-eval, no-new-func; в Python — проверки Bandit или Ruff на вызовы eval/exec.
  2. Помните про скрытые формы eval: new Function("...") в JavaScript, а в браузере еще setTimeout("строка") и setInterval("строка") (Node.js 24 строку в таймер не принимает и бросает TypeError); exec и compile в Python.
  3. В браузере — Content Security Policy с директивой script-src (или default-src) без 'unsafe-eval': тогда eval, new Function и строковые таймеры блокируются даже при ошибке в коде. Без самой директивы запрета нет.
  4. В Node.js флаг --disallow-code-generation-from-strings запрещает eval во всем процессе. Проверка на Node.js 24: eval("1 + 1") падает с EvalError: Code generation from strings disallowed for this context.
  5. Не считайте песочницей модуль node:vm или урезанные globals в Python: документация Node.js прямо говорит, что vm — не механизм безопасности. Если чужой код действительно нужно выполнять, запускайте его в отдельном процессе или контейнере с ограничением времени, памяти, сети и прав.
  6. Ограничения по объему и частоте запросов, журналирование отклоненного ввода и тесты со злым вводом: вызовы, глубокая вложенность, очень длинные числа.

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), отладчики, инструменты, которые генерируют код из доверенных шаблонов на этапе сборки. Общее у них одно — источник строки контролирует сам разработчик.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)