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

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #15

Related Articles

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.

critical

How Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.