Eval vs Function: Security in Sandboxed JavaScript

Executing dynamic JavaScript within web applications presents critical security challenges, particularly when isolating untrusted user input. While developers often compare eval() and the Function constructor for code interpretation, the two mechanisms handle scope, variable access, and execution contexts differently. This article examines the security distinctions between eval() and the Function constructor, analyzing their execution behavior, vulnerability surfaces, and limitations when building sandboxed environments.

Scope Resolution and Context Leaks

The most significant security difference between eval() and the Function constructor lies in lexical scope binding.

Direct calls to eval() execute within the caller's immediate lexical scope. This grants the evaluated code full read and write access to all local variables, enclosing closure scopes, and internal functions. If sensitive data—such as authentication tokens, user identifiers, or private state variables—exists within the enclosing function, untrusted code executed via direct eval() can inspect, leak, or modify those variables directly.

function secureContext() {
  const secretKey = "sensitive_token";
  // eval can access local variables directly
  eval("console.log(secretKey)"); 
}

Conversely, the Function constructor always executes in the global scope, regardless of where it is initialized or called. It does not inherit the local lexical scope of the calling context.

function secureContext() {
  const secretKey = "sensitive_token";
  // Throws ReferenceError: secretKey is not defined
  new Function("console.log(secretKey)")(); 
}

Because Function limits access strictly to the global namespace, it eliminates the risk of local closure leakage. However, an indirect eval() call (e.g., (0, eval)(code)) also executes in the global scope, mirroring the scoping behavior of Function.

Global Scope Exploitation

Neither eval() nor the Function constructor inherently provides a sandboxed environment. Even when dynamic code is restricted to the global scope via Function, it maintains full access to the browser's global object (window or globalThis).

A script executed via new Function() can:

  • Read and modify document.cookie or localStorage.
  • Manipulate the DOM to inject phishing forms, alter visual elements, or perform keylogging.
  • Intercept network requests by monkey-patching window.fetch or XMLHttpRequest.
  • Redirect users to malicious third-party websites using window.location.

Furthermore, even if the dynamic code is provided with a restricted set of arguments to mimic a clean scope, untrusted code can traverse prototypes to re-acquire the global object:

const sandboxFunction = new Function("context", "with(context) { return (function() { return this; })().constructor.constructor('return window')(); }");

Because functions generated by the Function constructor retain access to Object, Function, and the underlying prototype chains, attackers can break out of simulated wrappers.

Content Security Policy (CSP) Restrictions

From an enforcement standpoint, modern browser security treats eval() and Function identically under Content Security Policy (CSP).

Both APIs require the 'unsafe-eval' directive to function within an application. If an application enables 'unsafe-eval', it disables native browser protections against cross-site scripting (XSS) vectors that rely on dynamic string-to-code compilation. As a result, relying on Function instead of eval offers no security advantage against automated CSP auditing tools or defensive policies.

Architectural Requirements for Real Sandboxing

Because neither construct establishes an isolated security boundary, neither should be used in isolation to run untrusted code. True sandboxing requires strict process, context, or runtime boundaries:

  1. Sandboxed Iframes: Executing code inside an <iframe> configured with sandbox="allow-scripts" (omitting allow-same-origin) isolates the execution environment into a separate, null-origin context that cannot directly access the parent page's DOM, storage, or session tokens.
  2. Web Workers: Running dynamic code inside a dedicated Web Worker limits the untrusted script's reach by removing direct access to the DOM and window, confining access strictly to worker-safe APIs and message passing.
  3. Engine-Level Sandboxes: Employing systems like ShadowRealms or hardened JavaScript engines (e.g., WebAssembly-compiled interpreters) completely decouples execution from the host environment's prototype chains and global namespace.