EvalError در جاوااسکریپت یک کلاس استثنای تاریخی است که برای خطاهای مربوط به تابع eval طراحی شده بود، ولی در عمل تقریباً هیچ‌وقت توسط موتورهای امروزی پرتاب نمی‌شود و به یک «فسیل زنده» در spec تبدیل شده است. دانستن اینکه چرا این کلاس هنوز وجود دارد و چه چیزی به‌جای آن باید استفاده کنیم، بخش مهمی از درک جاوااسکریپت مدرن است. در این مقاله، تجربه‌ام از بررسی کدهایی که هنوز eval دارند را با شما به اشتراک می‌گذارم.

EvalError چیست و از کجا آمده؟

EvalError یکی از هشت کلاس استثنای داخلی جاوااسکریپت است که در spec اصلی این زبان تعریف شده، ولی در پیاده‌سازی‌های مدرن تقریباً هرگز پرتاب نمی‌شود. این کلاس، از ریشهٔ Error ارث می‌برد و ساختار ارث‌بری آن ساده است:

Error
 └── EvalError

در نسخه‌های اولیهٔ جاوااسکریپت (ECMAScript 1 و 2)، این کلاس برای نشان دادن خطاهای مرتبط با تابع eval() در نظر گرفته شده بود. در آن زمان، انتظار می‌رفت که موتور جاوااسکریپت وقتی در فرآیند اجرای کد پویا مشکلی پیش می‌آید، این خطا را پرتاب کند. مفهوم کلی این کلاس در ویکی‌پدیا ذیل JavaScript syntax قابل پیگیری است. برای درک چارچوب گسترده‌تر خطاهای جاوااسکریپت، مدیریت خطا در جاوااسکریپت و آموزش جاوااسکریپت از صفر را پیشنهاد می‌کنم.

EvalError در جاوااسکریپت مدرن، بیشتر شبیه یک «سند تاریخی» است تا یک خطای واقعی: وجود دارد، مستند شده، ولی تقریباً هیچ‌وقت اتفاق نمی‌افتد.

در عمل، اگر با یک خطای EvalError مواجه شدید، تقریباً همیشه یکی از این سه اتفاق افتاده است: خطای واقعی از SyntaxError، TypeError یا خطای CSP (Content Security Policy) است که پیام آن به اشتباه به‌عنوان EvalError تفسیر شده، یا در محیط اجرای خاصی (مثل Web Workers با محدودیت‌های شدید) کد شما نقض شده است. در ادامه، تمام این سناریوها را بررسی می‌کنیم.

تاریخچه: از ES1 تا امروز

برای درک کامل EvalError، باید تاریخچهٔ آن را بشناسید. در ECMAScript 1 (سال ۱۹۹۷)، هشت نوع خطای داخلی تعریف شده بود که EvalError یکی از آن‌ها بود. در آن زمان، انتظار می‌رفت که این خطا در این سناریوها پرتاب شود:

  • وقتی eval() با کد نادرست از نظر معنایی صدا زده شود
  • وقتی eval() در محدودیت‌های محیط اجرا نقض شود
  • وقتی کد اجراشده به منابعی دسترسی پیدا کند که مجاز نیست

اما در عمل، این تفکیک هرگز به‌درستی پیاده‌سازی نشد. در ECMAScript 5 (سال ۲۰۰۹)، مشخص شد که تمام این خطاها بهتر است با کلاس‌های دقیق‌تری مثل SyntaxError (برای سینتکس نادرست) و ReferenceError (برای ارجاع نامعتبر) بازنمایی شوند. از آن زمان، EvalError به کلاسی تبدیل شد که در spec باقی ماند ولی هرگز استفاده نشد. برای مطالعهٔ دقیق spec، مستندات رسمی TC39 مرجع اصلی است.

در جدول زیر، وضعیت فعلی هر کلاس خطا در جاوااسکریپت مدرن را می‌بینید:

کلاس خطاوضعیت فعلیپرتاب توسط موتور
Errorپایهبله
RangeErrorفعالبله
ReferenceErrorفعالبله
SyntaxErrorفعالبله
TypeErrorفعالبله
URIErrorفعالبله
AggregateErrorفعال (ES2021)بله
EvalErrorمیراثیخیر (تقریباً هرگز)

همین جدول، پاسخ کوتاه به «چرا EvalError هرگز نمی‌بینم» است. این کلاس در spec باقی مانده تا کد قدیمی که ممکن است instanceof EvalError را چک کند، همچنان کار کند؛ ولی موتورها هیچ‌وقت آن را پرتاب نمی‌کنند.

چرا امروز تقریباً هرگز پرتاب نمی‌شود؟

دو دلیل اصلی وجود دارد که EvalError را به یک کلاس میراثی تبدیل کرده است. اول، تغییر رویکرد موتورهای جاوااسکریپت در بازنمایی خطاها؛ و دوم، ماهیت متفاوت مسائل مربوط به eval() در دنیای امروز.

دلیل اول: تفکیک دقیق‌تر کلاس‌های خطا

در سال‌های اولیهٔ جاوااسکریپت، تفکیک دقیق خطاها چندان مهم نبود. ولی با رشد این زبان و پیچیده‌تر شدن پروژه‌ها، نیاز به تفکیک دقیق‌تر احساس شد. در ECMAScript 5، تصمیم گرفته شد که خطاهای مربوط به eval هم زیر کلاس‌های دقیق‌تری تقسیم شوند:

try {
    eval("var x = ;");
} catch (e) {
    console.log(e.name); // SyntaxError (نه EvalError)
}

try {
    eval("undefinedVariable");
} catch (e) {
    console.log(e.name); // ReferenceError (نه EvalError)
}

همان‌طور که می‌بینید، موتورهای مدرن، خطاهای داخل eval را با کلاس‌های دقیق‌تری نمایش می‌دهند. این تغییر، در دیباگ بسیار مفید است چون مستقیماً به ریشهٔ مشکل اشاره می‌کند.

دلیل دوم: محدودیت‌های CSP جایگزین EvalError شدند

در دنیای وب مدرن، وقتی مرورگر از اجرای eval جلوگیری می‌کند، معمولاً این کار را از طریق Content Security Policy (CSP) انجام می‌دهد و به‌جای EvalError یک Error عمومی یا TypeError پرتاب می‌کند:

// در مرورگر با CSP سخت‌گیرانه
try {
    eval("2 + 2");
} catch (e) {
    console.log(e.name); // Error یا TypeError، نه EvalError
    console.log(e.message); // "Refused to evaluate a string as JavaScript..."
}

نکتهٔ ظریف: پیام دقیق این خطا بسته به مرورگر متفاوت است ولی همیشه EvalError نیست. همین موضوع، منشأ سردرگمی بسیاری از توسعه‌دهندگان است که انتظار EvalError دارند.

مفهوم CSP و نقش آن در امنیت وب، در ویکی‌پدیا ذیل Content Security Policy توضیح داده شده است. برای مطالعهٔ بیشتر دربارهٔ رفتار مرورگرها در این حوزه، عیب‌یابی خطاهای JS در کنسول مرورگر نکات مکمل را ارائه می‌دهد.

تفاوت EvalError با SyntaxError و ReferenceError

برای تشخیص دقیق، باید EvalError را با کلاس‌های نزدیکش مقایسه کنید. در کد زیر، سه سناریوی مختلف که ممکن است با eval رخ دهند، بررسی شده است:

// سناریو ۱: سینتکس نامعتبر در کد string
try {
    eval("if (true) {");
} catch (e) {
    console.log(e.name); // SyntaxError
}

// سناریو ۲: ارجاع به متغیر تعریف‌نشده
try {
    eval("notDefined");
} catch (e) {
    console.log(e.name); // ReferenceError
}

// سناریو ۳: دستکاری محدودیت‌های CSP
try {
    eval("42");
} catch (e) {
    // در CSP strict: خطای Error یا TypeError، نه EvalError
    console.log(e.name);
}

تفاوت‌ها به‌طور خلاصه:

کلاس خطاچه زمانی پرتاب می‌شود
SyntaxErrorسینتکس کد string نامعتبر است
ReferenceErrorارجاع به متغیر یا تابع تعریف‌نشده
TypeErrorعملیات روی نوع اشتباه
EvalErrorتقریباً هرگز؛ در spec باقی مانده ولی استفاده نمی‌شود

در عمل، اگر با کدی مواجه شدید که سعی می‌کند EvalError را بگیرد، احتمالاً کد قدیمی است که در دههٔ ۲۰۰۰ نوشته شده و کسی به‌روزش نکرده. برای مطالعهٔ بیشتر دربارهٔ رفتار خطاهای هم‌خانواده، خطای SyntaxError در جاوااسکریپت نکات مرتبط را ارائه می‌دهد.

مشکلات جدی eval: امنیت، عملکرد، CSP

اگرچه EvalError در عمل پرتاب نمی‌شود، ولی خود تابع eval() سه مشکل جدی دارد که در پروژه‌های واقعی بارها دیده‌ام. آگاهی از این مشکلات، دلیل اصلی پرهیز از eval است.

مشکل اول: امنیت

تابع eval() هر رشته‌ای را به‌عنوان کد جاوااسکریپت اجرا می‌کند. اگر آن رشته از ورودی کاربر یا منبع خارجی بیاید، این یک آسیب‌پذیری جدی است — شبیه همان تزریق کد (code injection) که در پایتون با exec و eval دیده می‌شود. مفهوم این آسیب‌پذیری در ویکی‌پدیا ذیل Code injection توضیح داده شده است:

// اشتباه خطرناک
const userExpression = getUserInput(); // مثلاً "alert(document.cookie)"
eval(userExpression); // اجرای کد مخرب

این الگو در پروژه‌های calculator، form ساز پویا و سیستم‌های rule engine که ساده‌لوحانه از eval استفاده می‌کنند، بسیار رایج است. راه‌حل: هرگز ورودی کاربر را به eval ندهید. اگر به ارزیابی expression نیاز دارید، از کتابخانه‌های اختصاصی مثل mathjs یا پیاده‌سازی parser سفارشی استفاده کنید.

مشکل دوم: عملکرد

موتورهای جاوااسکریپت، کد را در چند مرحله کامپایل می‌کنند. کدی که از طریق eval می‌آید، نمی‌تواند از بهینه‌سازی‌های پیشرفته (مثل inline caching) بهره ببرد، چون موتور نمی‌داند چه کدی قرار است اجرا شود. نتیجه: کدی که در بلوک‌های معمولی سریع است، در eval کند می‌شود. این کندی در پروژه‌های پرمعامله، به‌طور مستقیم بر تجربهٔ کاربری اثر می‌گذارد.

در تست‌های عملی که انجام داده‌ام، تفاوت سرعت بین کد نوشته‌شده در eval و کد معمولی می‌تواند به دو تا پنج برابر برسد. برای مطالعهٔ بیشتر دربارهٔ بهینه‌سازی، بهینه‌سازی عملکرد جاوااسکریپت مرجع مکمل خوبی است.

مشکل سوم: ناسازگاری با CSP

در دنیای وب مدرن، اکثر سایت‌های حرفه‌ای از Content Security Policy استفاده می‌کنند. سیاست‌های CSP به‌طور پیش‌فرض eval را ممنوع می‌کنند:

Content-Security-Policy: script-src 'self'; object-src 'none'

در این حالت، هر فراخوانی eval بلافاصله با خطا متوقف می‌شود. اگر کتابخانه‌ای که استفاده می‌کنید به‌طور داخلی از eval استفاده کند (مثل بعضی templating engineهای قدیمی)، در سایت با CSP سخت‌گیرانه اصلاً کار نمی‌کند. برای غیرفعال کردن این محدودیت، باید 'unsafe-eval' را به CSP اضافه کنید که ریسک امنیتی بزرگی است.

در سال ۲۰۲۵، پرهیز از eval فقط توصیهٔ سبک نیست؛ یک شرط عملیاتی برای سازگاری با CSP و امنیت مرورگر مدرن است.

همین سه مشکل، دلیل اصلی است که تیم‌های حرفه‌ای جاوااسکریپت، eval را به‌عنوان یک anti-pattern می‌شناسند. برای مطالعهٔ بیشتر دربارهٔ امنیت جاوااسکریپت، مدیریت خطا در جاوااسکریپت نکات مرتبط را ارائه می‌دهد.

سناریوهای واقعی که به eval و EvalError می‌رسند

در طول سال‌ها کار با جاوااسکریپت، در این پنج سناریو دیده‌ام که توسعه‌دهندگان به eval روی می‌آورند و انتظار EvalError دارند (ولی خطای دیگری می‌گیرند).

سناریوی اول: rule engine پویا

در سیستم‌های rule engine، اگر قواعد به‌صورت string ذخیره شوند و با eval اجرا شوند:

// اشتباه خطرناک
function evaluateRule(rule, context) {
    return eval(rule); // rule از دیتابیس می‌آید
}

evaluateRule("context.age > 18", { age: 25 });

اگر یک مهاجم بتواند قاعده‌ای مخرب در دیتابیس بنویسد، کد دلخواه اجرا می‌شود. راه‌حل: استفاده از کتابخانه‌های اختصاصی expression evaluator مثل json-logic-js یا jsep که بدون eval کار می‌کنند.

سناریوی دوم: فرم ساز پویا

در فرم‌سازها، گاهی validation logic به‌صورت string ذخیره می‌شود:

// اشتباه
function validate(field, value, rule) {
    return eval(rule); // rule = "value.length >= 3"
}

راه‌حل: تبدیل قواعد به توابع نام‌دار با دیکشنری:

const VALIDATORS = {
    minLength: (value, n) => value.length >= n,
    email: (value) => /^[^@]+@[^@]+\.[^@]+$/.test(value),
};

function validate(field, value, rule) {
    const [name, ...args] = rule.split(":");
    return VALIDATORS[name](value, ...args);
}

سناریوی سوم: template engine ساده

بعضی template engineهای قدیمی، از eval برای ارزیابی expressionها استفاده می‌کنند. راه‌حل مدرن: استفاده از Template Literal یا template engineهایی مثل Handlebars که بدون eval کار می‌کنند.

سناریوی چهارم: parser ریاضی

در calculator یا parser فرمول، روی آوردن به eval ساده‌ترین راه است ولی ناامن‌ترین:

// اشتباه
function calculate(expression) {
    return eval(expression);
}

// با ورودی "process.exit(1)" یا مشابه، فاجعه
calculate("process.exit(1)");

راه‌حل درست: استفاده از mathjs که expression را در یک sandbox امن ارزیابی می‌کند:

import { evaluate } from "mathjs";

function calculate(expression) {
    return evaluate(expression); // امن، بدون eval
}

سناریوی پنجم: deserialize داده‌های معتبر

بعضی از پروژه‌ها، دادهٔ JSON ذخیره‌شده را با eval می‌خوانند (به‌جای JSON.parse). این کار از دوران قبل از JSON استاندارد مانده است:

// اشتباه
const data = eval("(" + jsonString + ")");

// درست
const data = JSON.parse(jsonString);

نکتهٔ امنیتی: استفاده از eval به‌جای JSON.parse، در پروژه‌هایی که داده از منبع خارجی می‌آید، فاجعه‌بار است. مهاجم می‌تواند کد مخرب را در قالب یک شیء JSON بفرستد.

این سناریوها نشان می‌دهد که دلایل استفاده از eval در اکثر موارد، به «عادت» یا «راحتی» برمی‌گردد، نه نیاز واقعی. برای مطالعهٔ بیشتر دربارهٔ الگوهای امن، دستکاری DOM در جاوااسکریپت نمونه‌های عملی خوبی را ارائه می‌دهد.

روش تشخیص در پنج گام

در برخورد با خطای مربوط به eval (که معمولاً EvalError نیست)، پروتکل زیر را در پروژه‌های خودم اجرا می‌کنم. در بیشتر پرونده‌ها، گام دوم یا سوم مقصر را روشن می‌کند.

گام اول: تشخیص نوع دقیق خطا

اگر در لاگ شما «EvalError» دیده می‌شود، اول مطمئن شوید که واقعاً این کلاس است و نه چیز دیگری:

try {
    eval(someString);
} catch (e) {
    console.log({
        name: e.name,
        message: e.message,
        constructor: e.constructor.name,
        isEvalError: e instanceof EvalError,
        isSyntaxError: e instanceof SyntaxError,
        isReferenceError: e instanceof ReferenceError,
    });
}

در ۹۹٪ موارد، isEvalError مقدار false خواهد بود و خطای واقعی SyntaxError یا ReferenceError است.

گام دوم: جستجوی eval در کد

برای یافتن تمام نقاط استفاده از eval در پروژه:

# در ترمینال یا CI
$ grep -rn "eval(" --include="*.js" --include="*.ts" ./src
$ grep -rn "new Function" --include="*.js" ./src
$ grep -rn "setTimeout("" --include="*.js" ./src

سه الگوی مهم: eval()، new Function() و setTimeout با string (که به‌طور داخلی مثل eval عمل می‌کند). این سه، هم‌ارزهای پنهان eval هستند و همه باید بازبینی شوند.

گام سوم: بررسی CSP در مرورگر

در کنسول DevTools مرورگر، به تب Network بروید و هدر پاسخ Content-Security-Policy را بررسی کنید:

Content-Security-Policy: script-src 'self'; object-src 'none'

اگر 'unsafe-eval' در این هدر نباشد، eval ممنوع است. حتی قبل از فراخوانی eval، مرورگر آن را با یک خطا مسدود می‌کند.

گام چهارم: بررسی bundle در production

گاهی کتابخانه‌های third-party از eval استفاده می‌کنند. برای یافتن این موارد در bundle production:

$ npx source-map-explorer dist/main.js
# یا
$ npx webpack-bundle-analyzer dist/stats.json

اگر در bundle خودکدهای eval یا new Function دیدید، باید کتابخانه‌ای که آن‌ها را تولید کرده، شناسایی و در صورت امکان جایگزین شود.

گام پنجم: بازتولید در sandbox

برای اطمینان از رفتار نهایی در مرورگر، کد را در یک sandbox تست کنید:

// در node --experimental-vm-modules
import vm from "vm";

const context = vm.createContext({});

try {
    vm.runInContext("eval('2 + 2')", context);
} catch (e) {
    console.log(e.name); // بسته به تنظیمات
}

این رویکرد، رفتار eval را در محیط کنترل‌شده نشان می‌دهد. برای مطالعهٔ بیشتر دربارهٔ امنیت اجرای کد، Promise در جاوااسکریپت نکات مرتبط را ارائه می‌دهد.

جایگزین‌های امن برای eval

در ۹۵٪ مواردی که توسعه‌دهندگان به eval روی می‌آورند، جایگزین امن و سریع‌تری وجود دارد. در ادامه، شش الگوی اصلی را بررسی می‌کنیم.

الگوی اول: JSON.parse به‌جای eval برای داده

// اشتباه
const config = eval("(" + jsonString + ")");

// درست
const config = JSON.parse(jsonString);

این ساده‌ترین و پرتکرارترین جایگزینی است. JSON.parse امن، سریع و استاندارد است.

الگوی دوم: دیکشنری توابع به‌جای expression

// اشتباه
function applyOperation(op, a, b) {
    return eval(`${a} ${op} ${b}`);
}

// درست
const OPERATIONS = {
    add: (a, b) => a + b,
    sub: (a, b) => a - b,
    mul: (a, b) => a * b,
    div: (a, b) => a / b,
};

function applyOperation(op, a, b) {
    const fn = OPERATIONS[op];
    if (!fn) throw new RangeError(`unknown operation: ${op}`);
    return fn(a, b);
}

این الگو، هم امن است و هم بسیار سریع‌تر از eval. اگر تعداد operations زیاد است، می‌توانید آن‌ها را در ماژول‌های جدا سازمان دهید.

الگوی سوم: new Function با محدودیت (فقط در آخرین گزینه)

در مواردی که واقعاً نیاز به کامپایل کد پویا دارید (مثل اجرای expression کاربر)، new Function گزینهٔ کم‌ریسک‌تری نسبت به eval است:

// new Function در scope global است، نه local
const fn = new Function("a", "b", "return a + b");
fn(2, 3); // 5

نکات: new Function به متغیرهای scope محلی دسترسی ندارد، پس از نظر امنیتی کمی بهتر از eval است. ولی همچنان با CSP سخت‌گیرانه سازگار نیست و باید فقط در شرایط کنترل‌شده استفاده شود.

الگوی چهارم: کتابخانه‌های expression evaluator

برای ارزیابی expressionهای ساده، کتابخانه‌های اختصاصی امن‌تری وجود دارند:

import { compile } from "mathjs";

const expr = compile("2 * x + 3");
const value = expr.evaluate({ x: 5 }); // 13

کتابخانه‌های مشابه: jsep برای پارس expression، json-logic-js برای قواعد ساخت‌یافته. این کتابخانه‌ها خودشان parser دارند و از eval استفاده نمی‌کنند.

الگوی پنجم: Template Literal به‌جای string concatenation

گاهی eval برای ساخت رشته‌های پویا استفاده می‌شود که با Template Literal قابل جایگزینی است:

// اشتباه
const greeting = eval("'Hello, ' + name + '!'");

// درست
const greeting = `Hello, ${name}!`;

الگوی ششم: Proxy و getter/setter به‌جای eval

در مواردی که می‌خواهید به‌صورت پویا به property دسترسی پیدا کنید، از Proxy یا getter استفاده کنید:

// اشتباه
const value = eval(`obj.${keyPath}`);

// درست
function getValue(obj, path) {
    return path.split(".").reduce((acc, key) => acc?.[key], obj);
}

const value = getValue(obj, "user.profile.name");

این الگو، امن و خوانا است و در پروژه‌های frontend زیاد استفاده می‌شود. برای مطالعهٔ بیشتر، شی گرایی در جاوااسکریپت نکات مکمل را ارائه می‌دهد.

در نود درصد پروژه‌هایی که به‌سراغ eval رفته‌اند، یک الگوی سادهٔ جایگزین وجود دارد. اگر به eval نیاز دارید، احتمالاً معماری به‌جای کد اشتباه است.

Content Security Policy و محدودیت eval

در دنیای وب مدرن، CSP یکی از مؤثرترین لایه‌های امنیتی است و eval را به‌طور پیش‌فرض ممنوع می‌کند. شناخت این محدودیت‌ها، برای هر توسعه‌دهندهٔ جدی ضروری است.

پایه‌های CSP و eval

CSP در ساده‌ترین شکل، یک هدر HTTP یا تگ meta است که منابع مجاز برای صفحه را تعیین می‌کند:

Content-Security-Policy: default-src 'self'; script-src 'self'

در این سیاست، script-src 'self' یعنی «اسکریپت‌ها فقط از همین دامنه». برای فعال کردن eval، باید 'unsafe-eval' اضافه شود:

Content-Security-Policy: script-src 'self' 'unsafe-eval'

افزودن 'unsafe-eval' امنیت را به‌شدت کاهش می‌دهد چون راه را برای حملهٔ XSS باز می‌کند. در پروژه‌های حساس، حتی به‌قیمت از دست دادن یک کتابخانهٔ قدیمی، نباید این کار را کرد.

محدودیت‌های اضافی: Web Workers و CSP

در Web Workers، CSP معمولاً سخت‌گیرانه‌تر است و eval به‌طور پیش‌فرض ممنوع. اگر کدی در Worker از eval استفاده کند، در اولین فراخوانی خطا می‌گیرید:

// در Web Worker
self.addEventListener("message", (event) => {
    try {
        const result = eval(event.data);
        postMessage(result);
    } catch (e) {
        postMessage({ error: e.name + ": " + e.message });
    }
});

این الگو در پروژه‌های واقعی دیده‌ام که با CSP سخت‌گیرانه و Web Workers، خطاهای عجیبی تولید می‌کند که به‌نظر EvalError می‌آید ولی در واقع خطای CSP است.

راه‌حل‌های سازگار با CSP

برای سازگاری با CSP سخت‌گیرانه، سه رویکرد وجود دارد:

  1. حذف eval: جایگزینی با الگوهای امن که در بخش قبل بررسی کردیم.
  2. nonce-based CSP: اجازه دادن به اسکریپت‌های خاص با nonce.
  3. Trusted Types: استاندارد جدید برای مقابله با DOM-based XSS که eval را کنترل می‌کند.

برای مطالعهٔ بیشتر دربارهٔ امنیت وب، مدیریت خطا در جاوااسکریپت نکات مرتبط را ارائه می‌دهد.

EvalError در React، Node و کتابخانه‌ها

هر فریم‌ورک و کتابخانه، رفتار خاصی با eval دارد. شناخت این رفتارها، در پروژه‌های واقعی حیاتی است.

React و JSX

React به‌طور داخلی از eval استفاده نمی‌کند، ولی بعضی کتابخانه‌های جانبی (مثل بعضی testing utilها یا DSL برای query) ممکن است. با CSP سخت‌گیرانه، این کتابخانه‌ها ممکن است در React خطا بدهند. راه‌حل: بررسی bundle با source-map-explorer.

Node.js و vm module

در Node.js، ماژول vm امکان اجرای کد در sandbox را می‌دهد بدون اینکه از eval مستقیم استفاده کند:

import vm from "vm";

const sandbox = { x: 2, y: 3 };
vm.createContext(sandbox);

const result = vm.runInContext("x + y", sandbox);
console.log(result); // 5

مزیت vm: sandbox مجزا، دسترسی محدود به scope، و کنترل بیشتر. برای اجرای کد پویا در سمت سرور، vm انتخاب اول من است.

Webpack و bundling

Webpack به‌طور پیش‌فرض از eval برای source maps سریع استفاده می‌کند (devtool: 'eval'). این تنظیم در production باید تغییر کند چون:

// در webpack.config.js
module.exports = {
    mode: "development",
    devtool: "eval-source-map", // فقط در development
};

module.exports = {
    mode: "production",
    devtool: "source-map", // در production
};

اگر در production از eval-source-map استفاده کنید، نه‌فقط bundle شما بزرگ می‌شود بلکه با CSP سخت‌گیرانه هم سازگار نیست.

Vue و Vue Template Compiler

Vue 2 در حالت runtime-compiler از new Function استفاده می‌کند. برای سازگاری با CSP، باید از vue با pre-compiled templates استفاده کنید:

// در Vue 3 (که پیش‌فرض pre-compiled است)، مشکلی نیست
import { createApp } from "vue";
import App from "./App.vue"; // pre-compiled

createApp(App).mount("#app");

نکته: در Vue 3 اگر از runtime-only build استفاده کنید، هیچ‌وقت به eval نیاز ندارد. برای مطالعهٔ بیشتر، async و await در جاوااسکریپت نکات مکمل را ارائه می‌دهد.

jQuery و کتابخانه‌های قدیمی

بعضی نسخه‌های قدیمی jQuery از new Function برای $.globalEval یا $.parseJSON استفاده می‌کردند. از jQuery 3.0 به بعد، $.parseJSON منسوخ شده و JSON.parse استاندارد شده است.

WebAssembly و ایمنی

WebAssembly (WASM) به‌عنوان جایگزین امن برای eval در پردازش‌های سنگین مورد توجه قرار گرفته است. WASM کد را در یک sandbox اجرا می‌کند و از بسیاری از آسیب‌پذیری‌های eval در امان است:

// به‌جای eval در پردازش‌های سنگین، از WASM استفاده کنید
const wasmModule = await WebAssembly.instantiate(bytes);
wasmModule.instance.exports.main();

مفهوم WASM و مزایای امنیتی آن، در ویکی‌پدیا ذیل WebAssembly توضیح داده شده است.

پرسش‌های پرتکرار درباره EvalError

این بخش، پرسش‌هایی را پوشش می‌دهد که در جلسات مشاوره و انجمن‌های فنی بیشترین تکرار را داشته‌اند. پاسخ‌ها به‌شکلی نوشته شده‌اند که برای جستجوهای مستقیم و دستیارهای هوش مصنوعی به‌عنوان پاسخ معتبر قابل استخراج باشند.

آیا EvalError در جاوااسکریپت مدرن هنوز وجود دارد؟

بله، کلاس EvalError در spec جاوااسکریپت باقی مانده است و می‌توانید با new EvalError() آن را بسازید و با instanceof EvalError بررسی کنید. ولی موتورهای مدرن (V8، SpiderMonkey، JavaScriptCore) هیچ‌وقت به‌طور خودکار این خطا را پرتاب نمی‌کنند. یعنی هرگز با پیام خطای «EvalError: ...» از سمت موتور روبرو نخواهید شد.

چرا EvalError در MDN به‌عنوان «Deprecated» علامت‌گذاری شده؟

چون هیچ‌وقت واقعاً استفاده نمی‌شود و همهٔ سناریوهای مربوطه با کلاس‌های دقیق‌تری مثل SyntaxError و ReferenceError پوشش داده می‌شوند. MDN این کلاس را برای حفظ سازگاری با کد قدیمی نگه داشته ولی توصیه می‌کند از آن استفاده نکنید. برای اطلاع از تاریخچه، صفحه MDN EvalError مرجع اصلی است.

آیا eval در Node.js هم ممنوع است؟

در Node.js، CSP به‌طور پیش‌فرض وجود ندارد، پس eval ممنوع نیست. ولی سه دلیل برای پرهیز از آن وجود دارد: اول، عملکرد پایین‌تر؛ دوم، خطر امنیتی در صورت دریافت کد از ورودی؛ سوم، سازگاری با ابزارهای bundling و linting. در Node، اگر به اجرای کد پویا نیاز دارید، از vm یا vm2 استفاده کنید.

چطور بفهمم از کجا eval در پروژه‌ام اجرا می‌شود؟

سه روش: اول، با grep در کد منبع (به‌دنبال eval، new Function، setTimeout با string). دوم، با ابزار تحلیل bundle مثل source-map-explorer که نشان می‌دهد کدام کتابخانه eval را وارد پروژه کرده. سوم، در مرورگر، با window.eval = () => { console.trace(); } که هر بار فراخوانی eval را trace می‌کند.

آیا new Function هم EvalError پرتاب می‌کند؟

خیر. new Function اگر سینتکس کد اشتباه باشد، SyntaxError پرتاب می‌کند. در حالت CSP سخت‌گیرانه، خطای Error یا TypeError می‌دهد، نه EvalError. یعنی همان الگوی خطاهای eval در این‌جا هم صدق می‌کند.

آیا می‌توان EvalError را catch کرد و به کار ادامه داد؟

فنی بله، ولی این کار تقریباً هیچ‌وقت لازم نمی‌شود چون EvalError پرتاب نمی‌شود. اگر واقعاً با خطای مرتبط با eval مواجه شدید، ابتدا نوع دقیق خطا (SyntaxError، ReferenceError یا Error مربوط به CSP) را تشخیص دهید و برای همان نوع catch بنویسید:

try {
    const result = eval(userInput);
} catch (e) {
    if (e instanceof SyntaxError) {
        // سینتکس اشتباه
    } else if (e instanceof ReferenceError) {
        // متغیر تعریف‌نشده
    } else if (e.name === "EvalError") {
        // تقریباً هرگز رخ نمی‌دهد
    } else {
        // CSP یا خطای ناشناخته
    }
}

آیا استفاده از eval در تست‌ها ممنوع است؟

در تست‌ها معمولاً محدودیت CSP وجود ندارد، پس eval کار می‌کند. ولی حتی در تست هم، عادت کردن به eval باعث ورود آن به کد production می‌شود. توصیه: هرگز eval را در تست هم استفاده نکنید، مگر برای شبیه‌سازی سناریوی خاصی که در production هم اتفاق می‌افتد.

چرا بعضی کتابخانه‌ها هنوز از eval استفاده می‌کنند؟

سه دلیل اصلی: اول، پشتیبانی از نسخه‌های بسیار قدیمی جاوااسکریپت (قبل از ES5). دوم، راحتی پیاده‌سازی در کتابخانه‌های DSL (مثل بعضی JSON query languages). سوم، در بعضی cases، سرعت بالاتر در کامپایل پویا (نادر). با این حال، هر کتابخانهٔ مدرنی که به‌طور جدی از eval استفاده می‌کند، باید با دقت بررسی شود.

آیا WebAssembly جایگزین کامل eval است؟

خیر. WASM برای اجرای کد کامپایل‌شده از زبان‌های دیگر (C، Rust) به‌کار می‌رود، نه برای eval پویا در زمان اجرا. اگر به کامپایل پویای کد جاوااسکریپت نیاز دارید، گزینه‌های محدودی دارید: new Function (که CSP را نقض می‌کند) یا کتابخانه‌های expression evaluator. برای بیشتر کاربردها، همان الگوهای جایگزینی که در بخش «جایگزین‌های امن» بررسی کردیم، کافی است.

آیا EvalError در Node.js و مرورگر رفتار متفاوتی دارد؟

خیر. کلاس EvalError استاندارد است و در همه محیط‌های اجرای جاوااسکریپت یکسان تعریف شده. تنها تفاوت، پیام‌های خطا در سناریوهای CSP است که مخصوص مرورگر است. در Node، چون CSP وجود ندارد، رفتار خطاهای eval دقیقاً همان رفتار مرورگرهای قبل از CSP است.

چگونه در ESLint، استفاده از eval را ممنوع کنیم؟

قاعدهٔ no-eval در ESLint این کار را می‌کند:

// .eslintrc.json
{
    "rules": {
        "no-eval": "error",
        "no-implied-eval": "error",
        "no-new-func": "error"
    }
}

قاعدهٔ no-implied-eval مخصوص setTimeout با string و مشابه‌های آن است. این سه قاعده را در همه پروژه‌های حرفه‌ای فعال کنید. برای اطلاعات بیشتر دربارهٔ ابزارهای توسعه، افزونه‌های ضروری مرورگر برای توسعه‌دهندگان مرجع مکمل خوبی است.

آیا eval در Web Workers مجاز است؟

در Web Workers بدون CSP، بله. با CSP سخت‌گیرانه، خیر. نکته: Web Workers حتی با CSP سخت‌گیرانه می‌توانند از importScripts استفاده کنند که گزینهٔ امن‌تری است:

// در worker.js
importScripts("utils.js"); // به‌جای eval

برای مطالعات مکمل دربارهٔ خطاهای جاوااسکریپت، خطای SyntaxError در جاوااسکریپت و خطای RangeError در جاوااسکریپت نکات مرتبط را ارائه می‌دهند.

آنچه پرهیز از eval به معماری کد من آموخت

EvalError بیش از یک کلاس خطای نادر، یک «درس طراحی» است. سه اصلی که پس از سال‌ها پرهیز از eval، در معماری کد خودم رعایت می‌کنم:

نخست، هر کد پویا، یک قرارداد امنیتی است. هر بار که کدی string را به eval یا مشابه آن می‌فرستید، در واقع یک قرارداد با خودتان می‌بندید: «این string از منبعی می‌آید که کنترل کامل روی آن دارم». در ۹۰٪ موارد، این قرارداد در محیط production نقض می‌شود. اگر به‌جای eval از دیکشنری توابع، کتابخانه‌های expression evaluator یا parser سفارشی استفاده کنید، این قرارداد را حذف می‌کنید و در عوض یک API صریح و امن می‌سازید.

دوم، CSP را از روز اول جدی بگیرید. بسیاری از تیم‌ها CSP را بعد از چند سال به پروژه اضافه می‌کنند و در آن لحظه متوجه می‌شوند که چند کتابخانه در bundle آن‌ها از eval استفاده می‌کنند. اضافه کردن 'unsafe-eval' برای اجازه دادن به این کتابخانه‌ها، نه‌فقط امنیت را کاهش می‌دهد، بلکه نقطهٔ ضعفی در کد ایجاد می‌کند که سال‌ها باقی می‌ماند. اگر از روز اول CSP سخت‌گیرانه فعال باشد، این مشکل هرگز پیش نمی‌آید.

سوم، ابزارهای lint را جدی بگیرید. قواعدی مثل no-eval و no-implied-eval در ESLint، گران‌بهاترین قواعد این ابزار هستند چون جلوی یک دسته کامل از باگ‌های امنیتی و عملکردی را می‌گیرند. در پروژه‌های خودم، این قواعد را از همان روز اول فعال می‌کنم و هر PR که نقضشان را داشته باشد، بدون بررسی رد می‌شود. عادت کوچک، جلوی فاجعه‌های بزرگ را می‌گیرد.

در پایان، اگر در پروژه‌ای با حالت خاصی از EvalError برخورد کردید که این‌جا پوشش داده نشده — مثلاً در ترکیب با کتابخانه‌های قدیمی، در محیط‌های sandbox خاص، یا در سرورهای Node با محدودیت‌های غیرمعمول — تجربه‌تان را در دیدگاه‌ها بنویسید. به‌ویژه اگر راه‌حلی متفاوت از رویکردهای معمول پیدا کرده‌اید که می‌تواند برای خوانندهٔ بعدی ارزشمند باشد. 🧭