Executive summary
What is opencode: opencode is a popular open-source AI coding agent with approximately 3 million weekly npm downloads and 211,000 GitHub stars. Like other coding agents, it can access local files, execute commands and interact with external systems, typically operating with the developer's privileges.
What we found: Pillar Security discovered a critical remote code execution (RCE) flaw in opencode affecting Windows and Windows Subsystem for Linux (WSL). The flaw allows an attacker-controlled remote Model Context Protocol (MCP) server to execute arbitrary code on a developer's machine during a remote MCP OAuth authentication process, without additional user interaction or consent.
Why it matters: MCP connects AI agents to external tools and services, making it an increasingly important part of enterprise AI development. This vulnerability demonstrates that simply connecting a coding agent to a compromised or malicious remote MCP server can expose a developer's machine to remote code execution. The attack does not require installing malicious software, exploiting the AI model or tricking it through prompt injection.
Broader implications: The same underlying vulnerability class had already been documented in three CVEs in 2025, yet it resurfaced in opencode. The research also demonstrates that commonly recommended protections, including URL scheme validation, are insufficient on their own. Even a seemingly legitimate HTTPS URL can carry the malicious payload.
Disclosure gap: Anomaly, opencode's developer, fixed the issue in version 1.18.33, released September 28, 2026, but did not issue a security advisory or request a CVE. As a result, conventional vulnerability scanners and dependency alerts will not identify affected installations, potentially leaving enterprises unaware that they need to upgrade.
What enterprises should do: Organizations running opencode on Windows or WSL should upgrade to version 1.18.33 or later and inventory affected installations directly. Security teams should also assess the trustworthiness of connected MCP servers and ensure that applications handling externally supplied URLs do not pass untrusted input into shell commands.
Crucially, the server did not need to be malicious when configured. The payload arrived in an HTTP response at connection time, so a later compromise, hijacked registry entry, typosquat, or expired domain could trigger the same path.

Three facts worth noting:
The ecosystem knew this class for years already. The identical defect, same package and OAuth field, was published as CVE-2025-6514 at 9.6 Critical in July 2025. Two more CVEs mapped the same sink that summer. opencode's vulnerable code was written in December 2025.
The common fix is not always right, which is the standard remedy URL scheme validation. Our payload is a perfectly ordinary https:// URL. Normal scheme validation does not stop it, nor does the MCP SDK's scheme denylist, which is still shipping as the default today.
Anomaly fixed the issue after our disclosure, but shipped it as a refactor. They also didn’t publish an advisory or request a CVE due to malicious remote MCP connections falling outside their threat model. Now up to date, no vulnerability scanner or dependency bot will flag it on affected products and pipelines. We are publishing this because otherwise nobody would and update/patch their opencode:
| Product | opencode (opencode-ai on npm) |
| Vendor | Anomaly (anomalyco) |
| Affected | >= 1.0.137, <= 1.18.32, on Windows and WSL |
| Patched | 1.18.33, released 2026-09-28 |
| CWE | CWE-78 (OS Command Injection), secondary CWE-88 |
| Proposed CVSS v3.1, although no public CVE has been published | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, 9.8 Critical |
Your agent talks to strangers
MCP is how agents reach external/internal systems: Jira, your database, cloud consoles, internal service someone wrapped in a server last quarter or more severely, an external service.
A remote MCP server is a third party protocol communicating to a process running on your laptop, which basically makes your MCP client a parser of untrusted input. Same category as a browser or a mail. Every field in every server response could be attacker-controlled until the client proves otherwise.
This is not how people talk have been talking about MCP security. For the past year, the conversation has been dominated by two vectors:
- Prompt injection, where a malicious server returns tool descriptions or general responses designed to manipulate the model. Prompt injection is real and mostly unsolved and deserves the attention it gets. Still valid for remote MCP to some extent.
- Malicious MCP package - a known supply chain risks of installing a version of package that contains malware (compromised) or was malicious from day 1. This vector is not valid for remote MCPs because nothing is being installed on your machine. It’s a configuration file that points to a remote service.
Where the bug lives
When an MCP server wants authorization, the client runs an OAuth discovery handshake. The server answers 401 with a WWW-Authenticate challenge pointing at its discovery documents, and those documents tell the client where to send the user to log in.
One of those fields is authorization_endpoint. It's a free-form string. The server being authenticated picks it.
In opencode 1.18.32 it went straight from the wire to a shell:
There was no scheme check on that path, there was no comparison against the origin of the server you configured. The whole handshake is automatic, because the entire point is to send you to a login page without making you copy-paste a URL and create friction.
Sink anyone?
open is a browser-launching package. 184 million downloads a week. You probably have it installed right now, several times over, whether you know it or not as a transitive dependency.
On macOS it runs open, on Linux, xdg-open and both through spawn with an argument vector, so there's no shell in that path.
On Windows it does something else. From open@10.1.2, the version every affected opencode release pinned:
247: encodedArguments.push(`"${options.target}"`); // target does not
256: options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');
Line 242 escapes the application name. Line 247 does not escape the target. The whole thing gets base64'd and handed to powershell -EncodedCommand.
PowerShell expands $(...) inside double-quoted strings. That's it. That's the bug.
PowerShell evaluates the subexpression before it resolves the command, so the payload hits even though the browser launch then fails. opencode throws that failure away.
How others fix this
The agreed-upon fix for this class is using URL scheme validation. Block the weird schemes, allow http: and https:, done. It's what the MCP TypeScript SDK ships. It's what opencode added in their patch. It's the obvious lesson of CVE-2025-6514.
Now look at our payload again. It's https://.
This isn't our observation, and it's worth saying so. tripodsan demonstrated it with https://$(calc.exe) in October 2023. Three CVEs and three years later, the ecosystem still reaches for scheme validation first.
JFrog's mcp-remote payload needed a nonexistent scheme (a:$(cmd.exe /c whoami ...)) for a specific reason: their command contained spaces, and new URL() percent-encodes spaces in a normal https URL, which breaks the payload. Pick an exotic scheme and you dodge the normalization.
Ours has no spaces. So it survives as a completely ordinary, well-formed https URL, and scheme-based control waves it through:
- The MCP SDK's denylist? Blocks
javascript:,data:,vbscript:. Ours ishttps:. Passes. - The stricter http/https allowlist the SDK considered and rejected? Ours is
https:. Passes. - opencode's new
openUrl()scheme check in 1.18.33? Passes.
What actually fixed opencode is the dependency bump. open@11.0.4 wraps the target in single quotes, and PowerShell doesn't expand anything inside single quotes. This is the true complete fix, and for opencode favor - true defense in depth.
Which produces a genuinely funny result. Anomaly's response to our report was "we've updated the dependency, but generally, trying to deal with a malicious MCP server is outside our threat model." What they dismissed as incidental is the what closed the hole. The scheme checks they also shipped, the ones that look like the real security fix, would not have stopped our payload.
If you implemented scheme validation after reading about CVE-2025-6514 and you're still on open@10.x, you are not fixed. You should check that. today.
It was reported in 2020 as a typo
Go look at issue #202 on the open repo, filed 2020-10-22.
Title: "URL with $ does not work on WSL 2."
Somebody noticed that if their URL had a $ in it, part of the URL went missing. They filed a bug about a broken link. They had found PowerShell string expansion, reported it as a cosmetic annoyance, and moved on, because why wouldn't you. It sat open for five years.
It was finally fixed on 2025-11-15 in open@11.0.0, commit 274d704, closing #318, #319 and #202 together. Release notes file it under "Fixes": Fix PowerShell argument escaping.
Somebody did connect it as a security related topic.
Issue #323, filed 2023-10-18 by tripodsan, an Adobe engineer. Title: "OS command injection on windows when opening urls." The proof of concept is one line:
Sindre answered six minutes later, quoting his own README:
This package does not make any security guarantees. If you pass in untrusted input, it's up to you to properly sanitize it.
It's almost impossible to make it entirely secure. That being said, I'm happy to merge pull requests to improve the escaping logic.
I went looking for whether that disclaimer had been written for the occasion. It hadn't. It went in on 2021-09-17 and it is byte-identical today. He was quoting a standing contract, accurately.
And he has a point. open is a launcher. Launching what you hand it is the entire API, the same way exec() runs what you hand it, and nobody files CVEs against exec(). The issue was labeled enhancement and help wanted, sat for 25 months, and was closed not_planned three days before 11.0.0 shipped the escaping anyway.
So the story isn't that nobody noticed. open said exactly what it was, in writing, for four years before any of this. Adobe read #323 and shipped their own URL validation in @adobe/aem-cli. Meta read #323 and added stricter validation to the React Native CLI. Everyone else imported it, handed it a remote string, and shipped.
opencode wrote await open(authorizationUrl) on 2025-12-07. Three weeks after the fixed version shipped. The 10.1.2 pin in packages/opencode/package.json was already a couple of months old by then and nobody revisited it for another ten months.
I don't think that's negligence. I think that's just what dependency pins do. Which is rather the point.
The SDK said no
By the time opencode wrote that line, this was not an obscure pattern. It was a pile-up.
- CVE-2025-6514 (2025-07-09), mcp-remote, 9.6 Critical, JFrog's Or Peles. Their writeup names
open()as the sink. - CVE-2025-58062 (2025-08-28), openmcp-client. Same
open()sink, Windows. - CVE-2025-10619 (2025-09-17), sequa-mcp. Same
redirectToAuthorizationposition.
Three CVEs with the same defect class, all before opencode's commit.
And in the middle of that, the MCP TypeScript SDK had the fix in hand. PR #841, opened 2025-08-05, proposed an http/https allowlist on authorization URLs. It was closed without merging. PR #877 went in instead on 2025-08-19 and shipped in SDK v1.17.4 with a denylist of three schemes: javascript:, data:, vbscript:.
That is still the policy in SDK 1.32.0 today. file: passes. Arbitrary schemes pass. And as we showed above, so does plain https: carrying a subexpression.
Who gets this right
It's two lines of code, and plenty of opencode's peers already wrote them. From our review of MCP client implementations:
- gemini-cli uses a scheme-gated
execFileopener. Nothing reaches a shell. - Claude Code guards the opener twice; the OAuth path can't reach it with a server-controlled value.
- github-mcp-server uses an argv opener and takes authorization servers from the operator instead of discovering them from the peer.
- Docker's mcp-gateway validates every OAuth endpoint uniformly behind an https allowlist, with an argv opener.
Note what the strong ones have in common, and it isn't the scheme check. It's argv. They never build a command line out of a remote string in the first place, so the question of what's in the string stops being load-bearing.
"Malicious MCP servers are out of scope" is a vendor position.
Anomaly's response
Credit to opencode, the fix was fast and better than it needed to be. PR #51414 merged 2026-09-25 as 29f07e0c, released in 1.18.33 three days later, with defense at three layers. open bumped to 11.0.4. A new openUrl() that rejects non-http(s). A scheme check in redirectToAuthorization itself, at the exact boundary we named in the report.
That's a thorough fix for a bug they maintain. Asked about the report, Anomaly's position was that malicious MCP server is outside of their threat model.
So how Anomaly acted on the report:
- The fix shipped as a refactor. PR #51414 is presented as consolidating eight browser-launch call sites. Neither it nor the release notes mention security.
- No CVE was requested or issued from their end.
So the vulnerability is fully public in the diff and completely invisible everywhere defenders would usually look. Anyone who reads v1.18.32...v1.18.33 can reconstruct it. Scanner will never flag it, because there's no CVE to match.
Disclosure timeline
- 2026-08-29: Reported privately via GitHub Security Advisory with full reproduction material.
- 2026-09-25: We follow up outside the advisory thread. Anomaly confirms it reached the team, then replies that malicious MCP servers are outside their threat model.
- 2026-09-25: PR #51414 merged, closing the vulnerability.
- 2026-09-25: We set out the distinction between this and prompt injection, ask them to reconsider and request a CVE, and say we intend to publish.
- 2026-09-28: 1.18.33 published to npm with the fix.
- 2026-10-04: We publish this report without a CVE.
What to do
Upgrade to opencode 1.18.33 or later.
Can't upgrade and you're on Windows or WSL? Remove unvetted and untrusted remote MCP servers out of your opencode config until you can. If you can’t, acknowledge and accept the exposure it creates.
If you maintain anything that opens URLs: don't hand open a remote string. Its README has said so since 2021 that it makes no security guarantees and that sanitizing is your job, and that is the actual contract regardless of which version you're on. 11.0.0 escapes the PowerShell target and earlier versions don't, so upgrading helps, but treat that as a seatbelt rather than the fix. And if you added scheme validation after CVE-2025-6514 and called it done, you are not done, because a plain https URL carries this payload fine.
For security teams: there's no CVE, so this will not show up in your scans or your dependency alerts. Finding it means inventorying opencode directly. Any version from 1.0.137 to 1.18.32 on a Windows or WSL host is affected.
Lessons for people building MCP clients
- Use argv, not a command line. This is the whole fix. If a remote string never gets interpolated into a shell, nothing else on this list matters very much.
- Scheme validation is not sufficient on its own. It's worth having. It would not have stopped us. Don't let it be your only control.
- Bind OAuth discovery to the configured origin. A server that names its own authorization endpoint is a server making your security decisions for you.
- Test the protocol surface on Windows, and any other supported OS.
- Every server-controlled field is untrusted input. Discovery documents, server metadata, tool and resource descriptors, error strings, the lot. The model is not the only consumer of what the server sent, and it is nowhere near the first.
Your MCP is a parser handling data from an untrusted remote peer, running unsandboxed with a developer's full privileges, and most of what it parses it handles long before the model wakes up.
Worth checking what yours does with a URL.
References
This advisory
- opencode repository ·
opencode-aion npm - Vulnerable commit, 2025-12-07
509e43d6 - Fix: PR #51414, merged as
29f07e0c, released in 1.18.33
The open package
sindresorhus/open· npm ·open@10.1.2source- Issue #202: "URL with
$does not work on WSL 2", 2020-10-22 - Issue #323: "OS command injection on windows when opening urls", 2023-10-18
- Issue #318 · #319: PowerShell argument escaping, 2023-07-24
- Fix commit
274d704, released in v11.0.0, 2025-11-15
Prior CVEs on the same sink
FAQs
Am I affected by the opencode MCP OAuth vulnerability?
If you run opencode version 1.0.137 through 1.18.32 on Windows or WSL and have remote MCP servers configured, yes. macOS and Linux are not affected because the browser launcher on those platforms does not pass the URL through a shell. Version 1.18.33, released on September 28, 2026, contains the fix.
What should I do right now?
Upgrade to opencode 1.18.33 or later. If you cannot upgrade and you are on Windows or WSL, remove any remote MCP servers you have not vetted from your opencode configuration until you can. Security teams should inventory opencode installations directly, because there is no CVE for scanners or dependency bots to match against.
Why is there no CVE for this vulnerability?
Anomaly, the vendor behind opencode, fixed the issue after our private report but did not request a CVE or publish a security advisory. Their position is that malicious MCP servers fall outside their threat model, and the fix shipped as a refactor of browser launch code. We are publishing this report so affected users know to update.
Is URL scheme validation enough to stop this attack?
No. The payload is an ordinary https URL, so checks that only allow http and https let it through, including the MCP TypeScript SDK's scheme denylist and the new scheme check opencode added. What closed the hole was upgrading the open package to a version that escapes the PowerShell target. The durable fix for any MCP client is to pass URLs as an argument vector and never build a shell command from a remote string.
Subscribe and get the latest security updates
Back to blog
.webp)
%20(1).webp)










