· tips  · 6 min read

The Dangers of eval: A Cautionary Tale

A deep dive into why eval and its cousins (new Function, setTimeout(string)) are dangerous, illustrated with real-world-style examples and concrete mitigations for web and Node.js applications.

A deep dive into why eval and its cousins (new Function, setTimeout(string)) are dangerous, illustrated with real-world-style examples and concrete mitigations for web and Node.js applications.

Outcome: by the end of this post you’ll be able to spot risky uses of eval in your codebase, understand real-world attack patterns that exploit it, and apply practical mitigations that remove the threat.

Why this matters - fast

You write JavaScript that runs in browsers and on servers. A single use of eval or a similar construct can turn a small bug into a full-blown remote code execution (RCE) or persistent cross-site scripting (XSS) incident. Short-term convenience can cost users, reputation, and downtime.

This is a cautionary tale. Read it now. Fix it later.

What eval actually does (and who its cousins are)

eval takes a string and executes it as JavaScript source in the current scope. It’s powerful. And it’s blind. It makes code out of text at runtime.

Closely related constructs that behave like eval:

  • new Function(codeString)
  • setTimeout(“codeString”, delay) and setInterval(“codeString”, delay)
  • Function.prototype.constructor when called with strings
  • Some template engines or deserializers that use those under the hood

For authoritative documentation on eval and why it’s dangerous, see the MDN page on eval and the MDN page on Content Security Policy guidance for ‘unsafe-eval’.

The practical risks - short list

  • Arbitrary code execution if attacker controls the string passed to eval/new Function.
  • Cross-site scripting (XSS) in browsers when untrusted input is interpolated into evaluated code.
  • Server-side RCE in Node.js if user input is deserialized with eval-like behavior.
  • Content Security Policy (CSP) bypasses if ‘unsafe-eval’ must be enabled.
  • Static-analysis and code-auditing blind spots: eval hides intent from linters and reviewers.

OWASP and security teams consider use of eval a high-risk pattern in most contexts. See OWASP recommendations for XSS prevention: https://cheatsheetseries.owasp.org/cheatsheets/XSS_Prevention_Cheat_Sheet.html

Two real-world-style scenarios (how things actually go wrong)

I’ll walk through two practical scenarios you may recognize. These are simplified and representative of real incidents that security researchers and incident responders see frequently.

Scenario A - Browser XSS via client-side template + eval

Imagine a quick template function someone wrote to render widgets. It stores a small expression in a template and uses new Function to evaluate it for speed:

// Dangerous: executing a user-supplied expression string
function renderWidget(data, expression) {
  // expression is expected like "price * 0.9"
  const fn = new Function('data', `with(data){ return ${expression} }`);
  return fn(data);
}

// Called with untrusted input
const userExpression = 'alert(document.cookie)'; // attacker-controlled
renderWidget({ price: 100 }, userExpression);

If an attacker can control expression, they can execute arbitrary JS in the page context - full XSS. This commonly appears where authors allow small dynamic snippets in UIs (filters, sort expressions, admin consoles) and trust them incorrectly.

Why this matters: once JS runs in a victim’s browser, anything authorized by that page (cookies, localStorage, DOM, authenticated API calls) is on the table.

Scenario B - Node.js deserialization & eval leading to RCE

On servers, a tempting shortcut is to deserialize an object stored as a string by using eval:

// Dangerous: using eval to deserialize arbitrary strings
function loadConfig(serialized) {
  // serialized may look like: "{ enabled: true, timeout: 500 }"
  return eval('(' + serialized + ')');
}

// If an attacker can change `serialized`:
loadConfig("(require('child_process').execSync('rm -rf /'))");

This turns attacker-controlled text into server-side code execution. In practice, vulnerabilities like this have led to full system compromise when untrusted data (from APIs, databases, or message queues) was processed without validation.

Note: Node’s vm module is sometimes suggested as a sandbox, but Node’s docs explicitly warn the vm module is not a security boundary - it is not a safe way to run untrusted code without careful isolation: https://nodejs.org/api/vm.html

How attacks look in the wild (patterns you can search for)

Attackers rarely send long, obvious payloads. They chain small things together. Look for these patterns in log files, telemetry, and code:

  • Strings that contain require(, process, child_process, exec, spawn, or document.cookie close to user-controlled inputs.
  • Unexpected serialization formats being passed to eval/new Function.
  • Use of string arguments to setTimeout/setInterval.
  • Third-party templates or libraries that call new Function under the hood (search your node_modules for “new Function(” or “eval(”).

Snyk and other security vendors maintain writeups on the danger of these functions and how common packages misuse them; see Snyk’s blog about dangerous functions in JavaScript as a starting point: https://snyk.io/blog/dangerous-functions-in-javascript/

Concrete safe alternatives (code you can use today)

  1. If you parse data: use JSON.parse, not eval.
// Safe deserialization
const obj = JSON.parse(userInput);
  1. If you need templating, use a safe templating engine (e.g., Handlebars, Mustache) that escapes by default and does not evaluate arbitrary JS expressions.

  2. For computed expressions from trusted sources only, avoid new Function; instead implement a small expression language or use a vetted sandbox library designed for untrusted expressions (and review its security claims).

  3. Never pass strings to setTimeout/setInterval. Pass functions.

// bad
setTimeout('doSomething()', 1000);

// good
setTimeout(doSomething, 1000);
  1. Enforce a restrictive Content Security Policy in browsers and avoid enabling ‘unsafe-eval’. That removes a class of attack vectors and forces safer coding patterns.

  2. For Node.js, treat vm as not a security boundary. If you must evaluate untrusted code, use a separate process, container, or an established sandboxing service with resource limits and strict OS-level isolation.

Finding and fixing eval in a legacy codebase

Actionable checklist:

  • Search your repo for eval(, new Function, Function(, setTimeout( with string literals, and suspicious instances of require(…) inside dynamic code.
  • Audit every occurrence: ask “can this string contain attacker-controlled data?” If yes - fix it.
  • Replace deserialization via eval with JSON.parse, structured formats, or explicit parsers.
  • Replace expression evaluation with a limited, well-tested expression interpreter (or whitelist allowed operations).
  • Add automated alerts: lint rules (eslint no-eval) and CI checks that fail builds when eval or new Function appear.
  • Add runtime detection: log or alert if risky functions are called unexpectedly in production.

ESLint rule: no-eval - https://eslint.org/docs/rules/no-eval

When you can’t remove eval immediately - mitigations

  • Validate and strictly whitelist inputs that reach eval. (This is fragile. Prefer removal.)
  • Add CSP that forbids ‘unsafe-eval’ so browser contexts can’t run string-eval; this protects your users even if your codebase still has problematic paths.
  • Ensure server-side code processing untrusted inputs runs with the least privilege. Limit filesystem and network permissions.
  • Monitor telemetry and alerts for unusual patterns like shell commands executed by server processes.

Detecting successful exploitation

Look for these indicators:

  • Unexpected child processes or shell commands spawned by your app.
  • Outbound network traffic from server components that don’t normally call external hosts.
  • Unusual changes in critical configuration files or database records.
  • Client-side: unexplained redirects, popups, script injections, or abnormal API calls originating from user sessions.

Final rules of thumb - short and actionable

  • Rule 1: Never evaluate strings that include user input.
  • Rule 2: Prefer data formats (JSON) and parsing libraries over code-generation.
  • Rule 3: Use templating libraries that escape by default. Don’t reimplement them with eval.
  • Rule 4: Treat any eval-like use as a security code smell and prioritize its removal.

Closing (and the real danger)

Eval is convenient. It feels clever. It makes dynamic problems easy.

But convenience collapses into catastrophe the moment user input finds its way into strings you treat as code.

Fix eval. Replace it with parsers, templates, and small, auditable interpreters. Add linting, CSP, and runtime checks. And remember: executing text as code is not a feature - it’s an attack surface. Avoid it.

References

Back to Blog

Related Posts

View All Posts »
Understanding the Event Loop: Myths vs. Reality

Understanding the Event Loop: Myths vs. Reality

Cut through the noise: learn what the JavaScript event loop actually does, why common claims (like “setTimeout(0) runs before promises”) are wrong, how browsers and Node differ, and how to reason reliably about async code.

Destructuring Function Arguments: A Cleaner Approach

Destructuring Function Arguments: A Cleaner Approach

Learn how to simplify and clarify JavaScript function signatures with parameter destructuring. See side-by-side examples, real-world patterns, pitfalls, and TypeScript usage to write more readable, maintainable code.