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
•Technically reviewed by Anupam Mediratta•Published September 11, 2026•Reviewed 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

picomatch 2.3.1 ReDoS: Extglob Pattern Catastrophic Backtracking

picomatch versions below 2.3.2, 3.0.2, or 4.0.4 contain a Regular Expression Denial of Service vulnerability in extglob pattern parsing. An attacker can cause catastrophic backtracking with patterns containing nested alternations and quantifiers, freezing any Node.js process that evaluates untrusted glob expressions.

critical

pet-window.js Dynamic Code Evaluation: CWE-94 Hardening via Number

The pet-window module constructed dynamic JavaScript by embedding raw configuration values into code strings. An attacker with local access could inject arbitrary JavaScript by modifying stored configuration. The fix replaces string interpolation with explicit Number() coercion and NaN validation for all numeric configuration parameters.

high

sanitizeUnicodeInput(): Fullwidth U+ Bypasses Codepoint Validation

The `sanitizeUnicodeInput()` helper used by the project character-range settings screen rewrote `U+` prefixes to `0x` and called `parseInt()`, but never normalized its argument first. Compatibility-equivalent forms such as fullwidth `U+`, superscript digits, or mathematical alphanumerics never matched the `/U\+/gi` regex, fell through to the `else return inputString` branch, and were handed back to callers verbatim as "sanitized" values. The fix inserts a `String.prototype.normalize('NFKC')` pas

high

brace-expansion Stack Exhaustion: CVE-2026-102276 Patched

A critical stack exhaustion vulnerability in brace-expansion allows attackers to crash Node.js applications by supplying specially crafted brace patterns that trigger unbounded recursion. The fix upgrades the library across all maintained version lines to enforce depth limits on recursive expansion. This vulnerability affects any service that expands user-controlled brace patterns without input validation.

critical

BFF Proxy QR Code Endpoint Prototype Pollution via Unvalidated JSON

A critical prototype pollution vulnerability in a backend-for-frontend (BFF) proxy endpoint allowed attackers to inject malicious properties into the JavaScript Object prototype by crafting JSON requests with forbidden keys. This could compromise application behavior across all objects. The fix adds explicit key validation to reject payloads containing `__proto__`, `constructor`, or `prototype`.

high

smol-toml 1.7.0 DoS: Malformed TOML Documents Crash Parser

A denial-of-service vulnerability in smol-toml 1.7.0 allows attackers to crash the parser by supplying malformed TOML documents. The vulnerability affects any application that parses untrusted TOML input. The fix, available in smol-toml 1.7.1, hardens input validation and error recovery.