CVE-2026-22813

Localhost to RCE: Poisoning the AI's Brain with CVE-2026-22813

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 14, 2026·6 min read

Executive Summary (TL;DR)

OpenCode, a popular open-source AI coding assistant, failed to sanitize Markdown output from LLMs in its web UI. By tricking a user into connecting their local agent to a malicious server, an attacker can inject JavaScript that pivots from XSS to full RCE, leveraging the agent's native ability to execute shell commands.

A critical Cross-Site Scripting (XSS) vulnerability in the OpenCode AI agent allows attackers to achieve Remote Code Execution (RCE) by hijacking the local web interface via unsanitized Markdown rendering.

The Hook: When Localhost Isn't Safe

There is a prevalent misconception in the developer tooling space that if a service listens on 127.0.0.1, it is inherently safe from the outside world. This fallacy is the bread and butter of modern browser-based exploitation. OpenCode is an AI agent—a tool explicitly designed to write code, modify files, and execute shell commands on your machine. It provides a slick web interface running on port 4096.

But here is the problem: The web interface trusts the data it displays implicitly. It assumes that the Large Language Model (LLM) on the other end is a benevolent, compliant robot. It doesn't account for the possibility that the 'brain' driving the UI might actually be a malicious server controlled by an attacker, or that a prompt injection could force a legitimate LLM to spit out malicious HTML.

This isn't just an alert box popping up. When you have XSS in a tool designed to interact with your operating system, you don't need a complex binary exploit chain. You just need a few lines of JavaScript to tell the backend: 'Hey, please run this shell command for me.' And the backend, designed to be helpful, will happily oblige.

The Flaw: Trusting the Output

The vulnerability (CWE-79) resides in the frontend's Markdown rendering engine. Like many modern chat interfaces, OpenCode takes the text stream from the AI model and converts it into rich HTML so you can see bold text, code blocks, and lists. However, the developers missed two critical lines of defense: Input Sanitization and Content Security Policy (CSP).

Specifically, the application takes the raw response from the LLM and feeds it directly into the DOM without passing it through a library like DOMPurify. This is the web equivalent of raw-dogging memory allocation without checks. If the text contains <script>alert(1)</script> or <img src=x onerror=...>, the browser executes it immediately.

To make matters worse, OpenCode included a 'feature' that allows the backend API URL to be overridden via a URL parameter (?server=...). This meant an attacker didn't even need to perform a Man-in-the-Middle attack or prompt injection; they simply had to send you a link to your own localhost instance pointing to their server.

The Code: The Smoking Gun

Let's look at the implementation logic. In the vulnerable version, the React component handling the chat messages looked deceptively simple. It used dangerouslySetInnerHTML, which, as the name implies, is dangerous if you don't know what you're doing.

Vulnerable Code (Conceptual):

// The chat component receives the 'content' from the AI
function ChatMessage({ content }) {
  // ❌ DIRECT RENDER: No sanitization applied
  const htmlContent = markdownLib.render(content);
  
  return (
    <div 
      className="prose"
      dangerouslySetInnerHTML={{ __html: htmlContent }} 
    />
  );
}

If content is manipulated to include an onerror handler, the game is over. The fix was straightforward but critical. The developers introduced DOMPurify to strip dangerous tags before rendering.

Patched Code:

import DOMPurify from 'dompurify';
 
function ChatMessage({ content }) {
  const rawHtml = markdownLib.render(content);
  // ✅ SANITIZED: Strips scripts, on* attributes, objects, etc.
  const cleanHtml = DOMPurify.sanitize(rawHtml);
  
  return (
    <div 
      className="prose"
      dangerouslySetInnerHTML={{ __html: cleanHtml }} 
    />
  );
}

This simple diff represents the difference between a helpful coding assistant and a remote root shell.

The Exploit: Chaining XSS to RCE

So how do we turn this into a shell? We chain the XSS with the application's intended functionality. Remember, OpenCode is designed to run commands. The frontend frequently makes POST requests to endpoints like /api/execute or /api/write-file. Since the XSS executes in the context of the origin (localhost:4096), we bypass Same-Origin Policy (SOP).

The Attack Chain:

  1. The Lure: The attacker sends the victim a link: http://localhost:4096/?server=http://evil-server.com.
  2. The Connection: The victim clicks. Their local OpenCode UI loads, but it connects to the attacker's LLM server instead of the local model.
  3. The Payload: The attacker's server waits for the initial "Hello" handshake and responds with a malicious Markdown payload:
    Welcome! <img src="x" onerror="fetch('/api/v1/execute', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ command: 'calc.exe' }) })" />
  4. The Execution: The React component renders the <img>. The image fails to load (src="x"), firing the onerror event.
  5. The Pivot: The JavaScript inside onerror executes fetch(). The browser sees a request from localhost:4096 to localhost:4096 and allows it. The backend receives the command calc.exe and executes it.

Here is the flow visualization:

The Impact: Game Over

This is a CVSS 9.4 for a reason. In a corporate environment, developers are high-value targets. They have SSH keys, AWS credentials, and access to source code repositories. Compromising a developer's machine often leads to supply chain attacks.

Because OpenCode runs with the permissions of the user (and potentially lacks authentication for local requests, as seen in the related CVE-2026-22812), the attacker effectively gains a terminal session on the victim's machine. They can exfiltrate ~/.ssh/id_rsa, inject backdoors into the projects the developer is working on, or pivot deeper into the corporate network.

This vulnerability highlights the fragility of 'local' web apps. It treats the browser as a trusted enclave, forgetting that the browser is the most hostile execution environment on the planet.

The Fix: Defense in Depth

The mitigation strategy adopted in version 1.1.10 is a textbook example of defense in depth. They didn't just fix the XSS; they hardened the environment.

  1. Sanitization: As shown in the code section, DOMPurify is now mandatory for all rendered markdown.
  2. Content Security Policy (CSP): The application now serves a CSP header that forbids unsafe-inline scripts. Even if the sanitizer failed, the browser would refuse to execute the injected JavaScript.
  3. URL Validation: The ability to override the backend server via a simple GET parameter has been removed or restricted. Connecting to a third-party server now requires explicit configuration changes in the UI settings, killing the 'drive-by' drive-by attack vector.

If you are running OpenCode < 1.1.10, update immediately. If you can't update, block port 4096 at the firewall level or run the agent inside a dedicated, network-isolated container.

Technical Appendix

CVSS Score
9.4/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
EPSS Probability
0.08%
Top 77% most exploited

Affected Systems

OpenCode AI Agent

Affected Versions Detail

Product
Affected Versions
Fixed Version
OpenCode
AnomalyCo
< 1.1.101.1.10
AttributeDetail
CWE IDCWE-79
CVSS Score9.4 (Critical)
Attack VectorNetwork
Privileges RequiredNone
User InteractionRequired (Clicking link)
EPSS Score0.00078
Exploit StatusPoC Available
CWE-79
Cross-site Scripting (XSS)

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.