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.cookieorlocalStorage. - Manipulate the DOM to inject phishing forms, alter visual elements, or perform keylogging.
- Intercept network requests by monkey-patching
window.fetchorXMLHttpRequest. - 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:
- Sandboxed Iframes: Executing code inside an
<iframe>configured withsandbox="allow-scripts"(omittingallow-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. - 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. - 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.