В JavaScript-приложении встречается eval(userInput) для выполнения выражений — опишите безопасные альтернативы (парсинг, sandbox, WebAssembly, безопасные интерпретаторы) и предложите конкретную реализацию выполнения ограниченного набора выражений
Кратко — зачем не использовать eval: он выполняет произвольный код в вашем контексте (получает доступ к глобальным объектам), сложно ограничить ресурсы и безопасность, уязвим к инъекциям. Основные безопасные альтернативы (суть и ограничения): - Парсинг выражений + собственный интерпретатор (самый гибкий и безопасный при аккуратной валидации). Подходит для арифметики, фильтров, простых функций. - Готовые парсеры + ограниченные рантаймы (например, jsep для парсинга + собственный эвальюатор; mathjs с sandbox-режимом). Быстро и удобно. - Изолированные сандбоксы: - Browser iframe с атрибутом sandbox (ограниченный доступ к DOM/окну). - Node: vm.Module / vm.runInContext или внешние пакеты типа vm2 (vm2 удобен, но следите за безопасностью пакетов). - SES (secure ECMAScript) / realms — для строгой изоляции. Ограничения: сложнее/трудоемко настроить, возможны уязвимости в реализующих библиотеках. - JS-интерпретаторы/песочницы: e.g. JS-Interpreter (Neil Fraser) — выполняет JS в контролируемой среде, можно запретить API и лимитировать шаги. - WebAssembly: подходит, если вы компилируете заранее написанный безопасный код (например, калькулятор). Не подходит для динамической интерпретации произвольных JS-выражений без дополнительной инфраструктуры. Рекомендация по большинству задач: если нужно выполнить только выражения (арифметика, функции, переменные) — парсинг + безопасный AST-эвальюатор. Это просто, быстро и безопасно контролируется. Конкретная реализация: парсинг через jsep + безопасный AST-эвальюатор, разрешающий только набор операторов и именованных функций/переменных. Поддерживаем: литералы, унарные/бинарные операции, вызовы разрешённых функций, идентификаторы (разрешённые переменные). Запрещаем: MemberExpression, ThisExpression, NewExpression, access к прототипам и любым именам типа "__proto__", "constructor". Пример (псевдокод/реализация на JS): ``` import jsep from 'jsep'; // Разрешённый набор функций и значений const SAFE_FUNCTIONS = { sin: Math.sin, cos: Math.cos, tan: Math.tan, sqrt: Math.sqrt, pow: Math.pow, abs: Math.abs, log: Math.log, max: Math.max, min: Math.min, // добавьте свои }; const SAFE_VALUES = { pi: Math.PI, e: Math.E }; function isDangerousIdentifier(name) { return name === '__proto__' || name === 'constructor' || name === 'prototype'; } function evalAst(node, scope = {}, depth = 0) { if (depth > 500) throw new Error('Max evaluation depth'); switch (node.type) { case 'Literal': return node.value; case 'Identifier': if (isDangerousIdentifier(node.name)) throw new Error('Forbidden identifier'); if (node.name in scope) return scope[node.name]; if (node.name in SAFE_VALUES) return SAFE_VALUES[node.name]; throw new Error(`Unknown identifier: ${node.name}`); case 'UnaryExpression': { const val = evalAst(node.argument, scope, depth + 1); switch (node.operator) { case '+': return +val; case '-': return -val; default: throw new Error(`Unsupported unary operator: ${node.operator}`); } } case 'BinaryExpression': { const a = evalAst(node.left, scope, depth + 1); const b = evalAst(node.right, scope, depth + 1); switch (node.operator) { case '+': return a + b; case '-': return a - b; case '*': return a * b; case '/': return a / b; case '%': return a % b; case '^': return Math.pow(a, b); // если парсер не выдаёт '^' как Binary, можно трансформировать case '**': return Math.pow(a, b); default: throw new Error(`Unsupported binary operator: ${node.operator}`); } } case 'CallExpression': { if (node.callee.type !== 'Identifier') throw new Error('Only simple function calls allowed'); const fname = node.callee.name; if (isDangerousIdentifier(fname)) throw new Error('Forbidden function name'); const fn = SAFE_FUNCTIONS[fname]; if (!fn) throw new Error(`Function not allowed: ${fname}`); const args = node.arguments.map(arg => evalAst(arg, scope, depth + 1)); return fn(...args); } default: throw new Error(`Unsupported expression type: ${node.type}`); } } // Внешняя функция для вычисления строки выражения function safeEvaluate(exprString, scope = {}) { // Простейшая валидация: запретить запрещённые символы/ключевые слова if (/process|require|global|window|document/.test(exprString)) { throw new Error('Forbidden token in expression'); } const ast = jsep(exprString); return evalAst(ast, scope); } // Пример использования: console.log(safeEvaluate('2*(3+4) + sin(pi/2)')); // = 14 + 1 = 15 console.log(safeEvaluate('pow(2, 10)')); // 1024 console.log(safeEvaluate('x + 2', { x: 5 })); // 7 ``` Замечания и рекомендации: - Обрабатывайте/очищайте вход: блокируйте ключевые слова и конструкции, которые ваш парсер может пропустить. - Запрещайте MemberExpression и вызовы с динамическими именами — это важно, чтобы исключить доступ к объектам через цепочки. - Ограничьте глубину рекурсии/количества шагов, чтобы избежать DoS через сложные выражения. - В продакшне рассмотрите использование аудита библиотек (jsep, vm2 и т. п.) и бенчмарков безопасности. - Если нужна полноценная песочница для произвольного JS — изучите vm2/SES/iframe+postMessage, но это сложнее, чем выражения-эвальюатор. Короткое резюме: для выполнения ограниченного набора выражений — используйте парсер (например, jsep) и безопасный AST-эвальюатор с белым списком операторов/функций/переменных и явными запретами на доступ к прототипам и member-выражениям. Это даст лучшее соотношение безопасности и простоты по сравнению с eval и большинством сандбоксов.
Основные безопасные альтернативы (суть и ограничения):
- Парсинг выражений + собственный интерпретатор (самый гибкий и безопасный при аккуратной валидации). Подходит для арифметики, фильтров, простых функций.
- Готовые парсеры + ограниченные рантаймы (например, jsep для парсинга + собственный эвальюатор; mathjs с sandbox-режимом). Быстро и удобно.
- Изолированные сандбоксы:
- Browser iframe с атрибутом sandbox (ограниченный доступ к DOM/окну).
- Node: vm.Module / vm.runInContext или внешние пакеты типа vm2 (vm2 удобен, но следите за безопасностью пакетов).
- SES (secure ECMAScript) / realms — для строгой изоляции.
Ограничения: сложнее/трудоемко настроить, возможны уязвимости в реализующих библиотеках.
- JS-интерпретаторы/песочницы: e.g. JS-Interpreter (Neil Fraser) — выполняет JS в контролируемой среде, можно запретить API и лимитировать шаги.
- WebAssembly: подходит, если вы компилируете заранее написанный безопасный код (например, калькулятор). Не подходит для динамической интерпретации произвольных JS-выражений без дополнительной инфраструктуры.
Рекомендация по большинству задач: если нужно выполнить только выражения (арифметика, функции, переменные) — парсинг + безопасный AST-эвальюатор. Это просто, быстро и безопасно контролируется.
Конкретная реализация: парсинг через jsep + безопасный AST-эвальюатор, разрешающий только набор операторов и именованных функций/переменных. Поддерживаем: литералы, унарные/бинарные операции, вызовы разрешённых функций, идентификаторы (разрешённые переменные). Запрещаем: MemberExpression, ThisExpression, NewExpression, access к прототипам и любым именам типа "__proto__", "constructor".
Пример (псевдокод/реализация на JS):
```
import jsep from 'jsep';
// Разрешённый набор функций и значений
const SAFE_FUNCTIONS = {
sin: Math.sin,
cos: Math.cos,
tan: Math.tan,
sqrt: Math.sqrt,
pow: Math.pow,
abs: Math.abs,
log: Math.log,
max: Math.max,
min: Math.min,
// добавьте свои
};
const SAFE_VALUES = {
pi: Math.PI,
e: Math.E
};
function isDangerousIdentifier(name) {
return name === '__proto__' || name === 'constructor' || name === 'prototype';
}
function evalAst(node, scope = {}, depth = 0) {
if (depth > 500) throw new Error('Max evaluation depth');
switch (node.type) {
case 'Literal':
return node.value;
case 'Identifier':
if (isDangerousIdentifier(node.name)) throw new Error('Forbidden identifier');
if (node.name in scope) return scope[node.name];
if (node.name in SAFE_VALUES) return SAFE_VALUES[node.name];
throw new Error(`Unknown identifier: ${node.name}`);
case 'UnaryExpression': {
const val = evalAst(node.argument, scope, depth + 1);
switch (node.operator) {
case '+': return +val;
case '-': return -val;
default: throw new Error(`Unsupported unary operator: ${node.operator}`);
}
}
case 'BinaryExpression': {
const a = evalAst(node.left, scope, depth + 1);
const b = evalAst(node.right, scope, depth + 1);
switch (node.operator) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return a / b;
case '%': return a % b;
case '^': return Math.pow(a, b); // если парсер не выдаёт '^' как Binary, можно трансформировать
case '**': return Math.pow(a, b);
default: throw new Error(`Unsupported binary operator: ${node.operator}`);
}
}
case 'CallExpression': {
if (node.callee.type !== 'Identifier') throw new Error('Only simple function calls allowed');
const fname = node.callee.name;
if (isDangerousIdentifier(fname)) throw new Error('Forbidden function name');
const fn = SAFE_FUNCTIONS[fname];
if (!fn) throw new Error(`Function not allowed: ${fname}`);
const args = node.arguments.map(arg => evalAst(arg, scope, depth + 1));
return fn(...args);
}
default:
throw new Error(`Unsupported expression type: ${node.type}`);
}
}
// Внешняя функция для вычисления строки выражения
function safeEvaluate(exprString, scope = {}) {
// Простейшая валидация: запретить запрещённые символы/ключевые слова
if (/process|require|global|window|document/.test(exprString)) {
throw new Error('Forbidden token in expression');
}
const ast = jsep(exprString);
return evalAst(ast, scope);
}
// Пример использования:
console.log(safeEvaluate('2*(3+4) + sin(pi/2)')); // = 14 + 1 = 15
console.log(safeEvaluate('pow(2, 10)')); // 1024
console.log(safeEvaluate('x + 2', { x: 5 })); // 7
```
Замечания и рекомендации:
- Обрабатывайте/очищайте вход: блокируйте ключевые слова и конструкции, которые ваш парсер может пропустить.
- Запрещайте MemberExpression и вызовы с динамическими именами — это важно, чтобы исключить доступ к объектам через цепочки.
- Ограничьте глубину рекурсии/количества шагов, чтобы избежать DoS через сложные выражения.
- В продакшне рассмотрите использование аудита библиотек (jsep, vm2 и т. п.) и бенчмарков безопасности.
- Если нужна полноценная песочница для произвольного JS — изучите vm2/SES/iframe+postMessage, но это сложнее, чем выражения-эвальюатор.
Короткое резюме: для выполнения ограниченного набора выражений — используйте парсер (например, jsep) и безопасный AST-эвальюатор с белым списком операторов/функций/переменных и явными запретами на доступ к прототипам и member-выражениям. Это даст лучшее соотношение безопасности и простоты по сравнению с eval и большинством сандбоксов.