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
processisundefined, notglobal.process - The parameter
requireisundefined, not the module's require function - The parameter
globalisundefined, 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 bycall()alone: Thecall()method controlsthisbinding, 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
AsyncFunctionconstructor provides structural isolation: Unlikeeval(), which executes in the current lexical environment,new Function()andnew 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.processorObject.freeze(global), which can be circumvented through prototype chains or clever property access. -
The construction of
coderemained unchanged: The fix demonstrates that vulnerability identification should focus on the execution sink (eval()) rather than the string construction logic. The ternary handling ofreturn (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.