Back to Blog
critical SEVERITY4 min read

eval() in Async Function Constructor Enables Runtime Escape

The eval.mjs command handler used raw `eval()` to execute JavaScript expressions, creating a critical code injection path if owner credentials are compromised. The fix replaces `eval()` with the `AsyncFunction` constructor and explicitly shadows `process`, `require`, and other runtime globals as parameters, preventing evaluated code from reaching the Node.js runtime even when authentication boundaries fail.

O
By Orbis AppSec
Published September 11, 2026Reviewed September 11, 2026

Answer Summary

The eval.mjs command handler in this first-party codebase used `eval(expression)` to execute owner-provided JavaScript. An attacker who compromises owner credentials gains full Node.js runtime access including `process.binding()`, filesystem operations, and arbitrary code execution. The fix replaces `eval()` with `new AsyncFunction("process", "require", "module", "global", "globalThis", "__dirname", "__filename", code)` which shadows dangerous globals as unusable parameters. CWE-89 (Improper Neutralization of Special Elements used in an SQL Command).

Vulnerability at a Glance

cweCWE-89
fixReplace eval() with AsyncFunction constructor and shadow dangerous globals as parameters
riskFull Node.js runtime compromise on credential theft
languageJavaScript (Node.js)
root causeeval() provided unrestricted access to lexical scope including require, process, and global
vulnerabilityCode Injection via eval()

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code)
Ecosystem Node.js
CVE / GHSA not assigned
CWE CWE-89 (Improper Neutralization of Special Elements used in an SQL Command)

The Vulnerability Explained

The runEval() function in the bot's evaluation command executed arbitrary JavaScript using a pattern that granted complete lexical access to the Node.js runtime:

result = await eval(`(async function() { ${code} })`).call(context);

This single line contained the critical flaw. By using eval() to construct and immediately invoke an async function, the evaluated code inherited the entire surrounding lexical environment—including require, process, module, global, and all Node.js internals.

The call(context) pattern attempted to provide a controlled this binding, but eval() does not respect scope isolation. Any code executed inside the eval() string can access variables from the outer scope through closure, and more critically, can reach process.binding('fs') or require('child_process') directly.

The authentication model restricted this command to bot owners, creating a dangerous single point of failure. Credential compromise through phishing, session hijacking, or configuration errors would grant immediate remote code execution capabilities—not just within the bot's permission scope, but full host-level access through Node.js process bindings.

Consider an attacker who obtains owner credentials and submits:

process.binding('fs').rmdirSync('/')

With the vulnerable eval() implementation, this executes without restriction. The eval() call runs in the context of the running Node.js process with all privileges of the bot's operating system user.

The Fix

The pull request replaces the eval()-based execution with a fundamentally different approach using the AsyncFunction constructor and explicit global shadowing:

// Before: unrestricted eval
result = await eval(`(async function() { ${code} })`).call(context);

// After: AsyncFunction with shadowed globals
const AsyncFunction = Object.getPrototypeOf(async function() {}).constructor;
result = await new AsyncFunction("process", "require", "module", "global", "globalThis", "__dirname", "__filename", code).call(context);

The key insight is how AsyncFunction handles its parameter list. When you invoke new AsyncFunction("process", "require", ..., code), JavaScript creates a function whose formal parameters shadow any same-named variables from outer scopes. Inside the function body:

  • The parameter process is undefined, not global.process
  • The parameter require is undefined, not the module's require function
  • The parameter global is undefined, not the global object

This shadowing occurs at the language level—there is no way for code inside the function to escape to the true globals without explicitly referencing something like Function('return this')() which would require constructing new function objects from strings, a pattern detectable and blockable through additional hardening.

The call(context) pattern is preserved, so the evaluated code still receives the intended context object as this, but the lexical environment is now stripped of dangerous capabilities. An attacker with compromised credentials can still execute JavaScript, but cannot trivially reach the filesystem, spawn processes, or access native bindings.

Key Takeaways

  • eval() cannot be secured by call() alone: The call() method controls this binding, not lexical scope. eval() executes code with access to all enclosing variables, making it unsuitable for any scenario where the code author is not fully trusted.

  • The AsyncFunction constructor provides structural isolation: Unlike eval(), which executes in the current lexical environment, new Function() and new AsyncFunction() create functions with their own scope chain. Declaring dangerous names as parameters exploits this to create undefeatable shadows.

  • Owner-only endpoints are high-value targets: Authentication boundaries fail. Designing commands that become catastrophic on credential compromise violates defense-in-depth principles. Owner-only code execution should still operate in constrained environments.

  • Parameter shadowing is a JavaScript security primitive: The language guarantees that function parameters shadow outer scope variables. This is more reliable than delete global.process or Object.freeze(global), which can be circumvented through prototype chains or clever property access.

  • The construction of code remained unchanged: The fix demonstrates that vulnerability identification should focus on the execution sink (eval()) rather than the string construction logic. The ternary handling of return ( wrapping was not the security boundary.

How Orbis AppSec Detected This

Source: The expression parameter passed to runEval() from bot command invocation, restricted to owner authentication but accepting arbitrary JavaScript strings.

Sink: eval() invoked with a template literal constructing an async function wrapper: eval(`(async function() { ${code} })`).

Missing control: No sandboxing, VM2 isolation, or scope isolation prevented evaluated code from accessing require, process.binding(), or other Node.js runtime capabilities. The call(context) pattern provided insufficient isolation.

CWE: CWE-89 (Improper Neutralization of Special Elements used in an SQL Command) — though the immediate mechanism is code injection rather than SQL injection, the classification reflects improper neutralization of special elements in data used by an interpreter.

Fix: Replace eval() with new AsyncFunction("process", "require", "module", "global", "globalThis", "__dirname", "__filename", code) to execute code in a scope where dangerous globals are shadowed by undefined parameters.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This vulnerability illustrates why eval() has no safe usage pattern in server-side JavaScript. Even with owner-only authentication, the function grants execution capabilities that transcend the application's security model. The AsyncFunction constructor with explicit global shadowing provides a surgical fix: preserving the command's functionality while eliminating the runtime escape path. For developers maintaining bot frameworks or any system with privileged evaluation endpoints, the lesson is to assume authentication will fail and design execution environments that are safe regardless.

Prevention and further reading

Frequently Asked Questions

Why shadow `process`, `require`, and `global` as AsyncFunction parameters rather than simply deleting them from the context object?

The `AsyncFunction` constructor creates a function whose parameters shadow any same-named variables in outer scopes. By declaring these dangerous globals as parameters, they become undefined inside the function body—even if `global.process` exists, the parameter `process` (undefined) takes precedence. Deleting from context alone wouldn't block lexical access to true globals.

Does the fix preserve the ability to use `await` and `async` syntax in evaluated expressions?

Yes. The `AsyncFunction` constructor inherently supports async/await, and the fix maintains the same `async function() { ... }` wrapper structure. The `await` keyword works identically; only access to runtime globals is restricted.

Is the `code` variable still constructed with the same `return (${expression});` or raw `expression` logic as before?

Yes. The ternary that prepends `return (` for expressions remains unchanged. The vulnerability was in the execution mechanism (`eval()`), not the code string construction.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4

Related Articles

high

How Regular Expression Denial of Service (ReDoS) Happens in Node.js trim-newlines and How to Fix It

CVE-2021-33623 exposed a Regular Expression Denial of Service (ReDoS) vulnerability in the npm package `trim-newlines` versions 1.0.0 and earlier. The vulnerable `.end()` method used an inefficient regex pattern that could cause severe performance degradation when processing malicious input. Upgrading to version 4.0.1 patches the regex implementation and eliminates the attack surface.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.

critical

How Unvalidated Dynamic Component Loading happens in TypeScript/Viewi and how to fix it

A critical vulnerability in Viewi's component loader allowed attackers to inject malicious JavaScript through compromised or MITM-attacked external component servers. The fix adds proper HTTP response validation before parsing dynamically fetched JSON components.

high

How Denial of Service via Crafted ZIP File happens in Node.js and how to fix it

CVE-2026-39244 is a high-severity denial of service vulnerability in the adm-zip npm package that allows attackers to crash Node.js applications by uploading maliciously crafted ZIP files. The fix upgrades adm-zip from version 0.5.16 to 0.6.0, which adds proper memory bounds checking to prevent excessive allocation during archive extraction.

critical

How prototype pollution happens in JavaScript AST traversal and how to fix it

A critical prototype pollution primitive was fixed in `src/traverse/estraverse` where visitor-supplied child keys were merged with `Object.assign(Object.create(this.__keys), visitor.keys)`. Because `Object.assign` uses assignment semantics, a key literally named `__proto__` reached the `Object.prototype` setter and rewired the prototype chain of the traversal key map instead of being stored as data. The fix replaces the merge with an object spread (`{ ...VisitorKeys, ...visitor.keys }`), which *

critical

How stored XSS happens in TinyMCE plugins and how to fix it

The snippets plugin's `Main.ts` inserted raw, unsanitized snippet content directly into the TinyMCE editor via `editor.insertContent(snippet.content)`, allowing stored JavaScript payloads saved by any snippet editor to execute in every user's browser. The fix routes snippet content through TinyMCE's own parser and serializer before insertion, stripping dangerous markup while preserving legitimate formatting.