Back to Blog
critical SEVERITY6 min read

How unsafe eval() code execution happens in JavaScript game scripting and how to fix it

A critical arbitrary code execution vulnerability was discovered in `scripts/CommandBlock.js` where user-provided input from a text dialog was directly concatenated into an `eval()` call without any sanitization or sandboxing. The fix replaces the dangerous `eval()` with a `new Function()` constructor, which provides better scope isolation and eliminates the string concatenation injection vector.

O
By Orbis AppSec
Published July 28, 2026Reviewed July 28, 2026

Answer Summary

This is an unsafe eval() vulnerability (CWE-95: Eval Injection) in JavaScript game scripting code where user input from a command block dialog is directly concatenated into an eval() statement, enabling arbitrary code execution. The fix replaces `eval("try{ " + text + "} catch(e) {...}")` with `try { (new Function(text))(); } catch(e) {...}`, which eliminates the string concatenation vector and provides scope isolation by not exposing local variables to the executed code.

Vulnerability at a Glance

cweCWE-95
fixReplace eval() with new Function() constructor wrapped in proper try/catch
riskComplete arbitrary code execution within the game's scripting environment
languageJavaScript
root causeUser input directly concatenated into eval() without sanitization
vulnerabilityEval Injection / Arbitrary Code Execution

How unsafe eval() code execution happens in JavaScript game scripting and how to fix it

Introduction

In the CommandBlock.js script of a Mindustry game mod, we discovered a critical arbitrary code execution vulnerability at line 209. The script handles in-game command blocks — interactive elements that let players trigger actions — and includes a "run-javascript" option that passes user input directly into eval(). The vulnerable line concatenated player-provided text into an eval string: eval("try{ " + text + "} catch(e) { Vars.ui.showText(error,e)}"). This pattern gives any player who interacts with the command block full access to the game's JavaScript runtime, including game state manipulation, rule changes, and potential denial-of-service attacks.

For developers building modding systems, game scripting engines, or any application that accepts user-provided code, this vulnerability illustrates exactly why eval() with string concatenation is considered one of the most dangerous patterns in JavaScript.

The Vulnerability Explained

The CommandBlock.js file handles tap events on command block entities. When a player selects the "run-javascript" option from the command block menu, a text input dialog appears. Whatever the player types is stored in the text variable and then executed:

const error = Core.bundle.format("commandblock.showtoast.run-javascript-2");
lastCommand = text;
eval("try{ " + text + "} catch(e) { Vars.ui.showText(error,e)}");

There are two critical problems here:

1. Direct string concatenation into eval(): The user's input (text) is concatenated directly into the eval string. This means a player can break out of the intended try block structure. For example, inputting } finally { /* malicious code */ } // would alter the control flow entirely.

2. Full local scope access: When eval() runs, it has access to the entire local scope — including the error variable, lastCommand, and any other variables in the enclosing Events.on(EventType.TapEvent, e => {...}) handler. This means executed code can modify the event handler's behavior for subsequent invocations.

Concrete Attack Scenarios

A player accessing the command block could enter:

  • Game rule manipulation: Vars.state.rules.editor = true; — enables editor mode, bypassing intended game restrictions
  • Denial of service: while(true){} — freezes the game with an infinite loop
  • State corruption: Vars.state.wave = 99999; — skips to an impossible wave, corrupting game progression
  • Scope escape: } catch(x){} Vars.state.rules.attackMode = true; try{ — breaks out of the try block structure to execute code outside the intended error handling

Because the game's scripting environment exposes Vars, Core, Sounds, and other powerful APIs, the impact extends to anything the game engine can do.

The Fix

The fix at line 211 replaces the dangerous eval() concatenation with a new Function() constructor wrapped in a proper try/catch block:

Before (vulnerable):

eval("try{ " + text + "} catch(e) { Vars.ui.showText(error,e)}");

After (fixed):

try { (new Function(text))(); } catch(e) { Vars.ui.showText(error, e); }

This change provides three specific security improvements:

1. Eliminates string concatenation injection: The user's input is no longer concatenated into a code string. It's passed as the body of a new function, meaning the player cannot break out of the try/catch structure by injecting } characters.

2. Scope isolation: new Function() creates a function in the global scope, not the local scope. The executed code cannot access error, lastCommand, e (the tap event), or any other local variables from the event handler. This significantly reduces the attack surface.

3. Proper error handling: The try/catch is now a real JavaScript control structure, not a string that gets parsed. This means error handling works correctly regardless of what the user inputs — even syntax errors are caught properly, whereas the old eval approach could fail to catch errors if the user's input broke the try/catch string structure.

Note that while new Function() still allows code execution (which is the intended feature of the "run-javascript" command block), it does so with proper isolation and without the injection amplification that string concatenation into eval creates.

Prevention & Best Practices

Avoid eval() entirely

The ESLint no-eval rule should be enabled in any JavaScript project. In virtually every case where eval() is used, a safer alternative exists:

Instead of Use
eval("obj." + prop) obj[prop]
eval("try{" + code + "}...") try { new Function(code)() }
eval(jsonString) JSON.parse(jsonString)

For game scripting systems specifically:

  1. Allowlist available APIs: If possible, create a sandboxed context that only exposes specific game APIs rather than the entire runtime
  2. Use Web Workers or iframes: For browser-based games, execute user code in an isolated context
  3. Implement timeouts: Wrap user code execution with setTimeout checks or use a termination mechanism to prevent infinite loops
  4. Input validation: Even with new Function(), consider validating input against known dangerous patterns

Detection tools:

  • ESLint: Enable no-eval and no-new-func rules
  • Semgrep: Use rules targeting eval() with tainted input
  • CodeQL: JavaScript security queries detect eval injection patterns

Key Takeaways

  • Never concatenate user input into eval() strings — the "try{ " + text + "} catch(e){...}" pattern in CommandBlock.js allowed players to break out of the intended control flow structure
  • new Function() provides scope isolation that eval() does not — the fix prevents access to local variables like error, lastCommand, and the tap event object e
  • String-based try/catch is fragile — wrapping eval'd code in a string try/catch can be bypassed by injecting closing braces; real try/catch blocks cannot be escaped this way
  • Game scripting features are attack surfaces — even in single-player contexts, crafted save files or shared maps can contain malicious command block configurations that execute when loaded
  • The threat model matters — this game mod requires loading a crafted asset to exploit, but the fix is still critical because players share maps and save files

How Orbis AppSec Detected This

  • Source: User text input from the in-game command block dialog (the text variable populated from the text input UI at the "run-javascript" option)
  • Sink: eval("try{ " + text + "} catch(e) { Vars.ui.showText(error,e)}") at scripts/CommandBlock.js:211
  • Missing control: No input sanitization, validation, allowlisting, or scope isolation between user input and code execution
  • CWE: CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code / Eval Injection)
  • Fix: Replaced eval() with string concatenation with new Function(text) wrapped in a native try/catch block, eliminating the injection vector and providing scope isolation

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 in CommandBlock.js demonstrates why eval() with string concatenation remains one of the most dangerous patterns in JavaScript — even in contexts like game scripting where code execution is intentional. The critical distinction is between controlled code execution (isolated scope, proper error handling, no injection amplification) and uncontrolled code execution (local scope access, breakable control flow, string injection). By switching to new Function() with a native try/catch, the fix preserves the intended "run JavaScript" feature while eliminating the security amplification that made the original implementation exploitable beyond its intended purpose.

If you maintain game mods, scripting engines, or any code that dynamically executes user input, audit your use of eval() today — and consider whether new Function(), a sandboxed iframe, or a purpose-built interpreter would better serve your security requirements.

References

Frequently Asked Questions

What is eval injection?

Eval injection occurs when untrusted input is passed to an eval() function, allowing an attacker to execute arbitrary code in the application's runtime context with full access to local scope variables and application state.

How do you prevent eval injection in JavaScript?

Avoid eval() entirely when possible. If dynamic code execution is required, use the Function constructor for scope isolation, implement input validation/allowlisting, or use a sandboxed execution environment like a Web Worker or iframe sandbox.

What CWE is eval injection?

CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code, also known as 'Eval Injection'). Related entries include CWE-94 (Improper Control of Generation of Code) and CWE-79 (Cross-site Scripting).

Is replacing eval() with new Function() enough to prevent code injection?

new Function() provides better scope isolation (it doesn't access local variables) and eliminates string concatenation injection, but it still executes arbitrary code. For full protection, input validation, allowlisting, or sandboxing should also be applied depending on the threat model.

Can static analysis detect eval injection?

Yes, static analysis tools like Semgrep, ESLint (no-eval rule), and specialized security scanners can detect direct eval() usage and flag cases where user input flows into eval statements. Pattern-based rules are highly effective for this vulnerability class.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #15

Related Articles

critical

How Plaintext Credential Storage happens in JSON Configuration Files and how to fix it

A critical security issue was discovered in `assets/settings/global.json` where a real phone number (PII) was stored in plaintext alongside placeholder patterns for API keys and payment credentials. This design encouraged developers to substitute real credentials directly into a version-controlled file, creating a high risk of credential exposure via repository access or filesystem reads. The fix replaces the hardcoded phone number with a placeholder and reinforces safe configuration patterns.

high

How Quadratic CPU Consumption Vulnerabilities Happen in JavaScript YAML Parsers and How to Fix Them

A high-severity denial-of-service vulnerability in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through specially crafted YAML documents using the !!omap tag. This fix upgrades js-yaml from 4.1.1 to 4.3.1 and from 3.14.2 to 3.15.1, eliminating the algorithmic complexity attack vector that could freeze Node.js applications processing untrusted YAML input.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `scripts/build.js` where `execSync` was called with string-interpolated arguments (`sourceDir` and `outputPath`) inside a shell command. By replacing `execSync` with `spawnSync` using an argument array (no shell), the fix eliminates the possibility of shell metacharacter injection while preserving identical build behavior.

high

How Command Injection happens in Node.js child_process and how to fix it

A command injection vulnerability in nix.js's Release class allowed potentially malicious input through the `arch` parameter to be executed via shell commands. The fix replaced `execSync()` with `execFileSync()`, eliminating shell interpretation and preventing command injection by passing arguments as an array instead of a concatenated string.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.