# When MCP sign-in runs code: authorization URL injection in opencode and mcp-remote

> How a malicious MCP server turned OAuth sign-in into code execution in opencode and mcp-remote, and how MCP clients should validate and open auth URLs.

- Published: 2026-10-27
- Author: Enforster AI (https://enforster.ai)
- Category: AI Security
- Tags: AI Security, MCP, Secure Coding, Supply Chain, Security Operations
- Canonical URL: https://blog.enforster.ai/mcp-oauth-authorization-url-command-injection

## Key takeaways

- In MCP's OAuth flow the remote server controls the authorization_endpoint URL, so the client must treat that URL as untrusted input from a possibly malicious server.
- opencode 1.0.137 through 1.18.32 passed a server-supplied authorization URL to the open npm package, which on Windows and WSL placed it unescaped inside a PowerShell command; 1.18.33 fixed it on September 28, 2026, with no CVE issued.
- A scheme allowlist alone would not have stopped the opencode payload, because it was a valid https URL carrying a PowerShell $(...) subexpression; the fix that closed the hole was upgrading open to 11.0.4.
- The MCP security best practices state that clients must only allow http and https authorization URLs and must not use shell commands such as cmd.exe, sh, or PowerShell to open them.
- Because the opencode flaw has no CVE, vulnerability scanners will not flag it, so teams need to check installed opencode versions and configured remote MCP servers directly.

---

When an MCP client signs in to a remote MCP server over OAuth, the server tells the client which authorization URL to open in the browser. If the client hands that URL to a shell, a malicious server can make the "open the browser" step run commands on the developer's machine. That is what happened in opencode on Windows and WSL (fixed in 1.18.33, with no CVE) and, a year earlier, in `mcp-remote` (CVE-2025-6514). The fix is to treat every server-supplied URL as untrusted input: allowlist the scheme and the expected authorization server, and launch the browser without a shell.

This is a narrow bug class, but it matters because sign-in is a step developers expect to be safe. It happens before any tool is called, so tool-approval prompts never see it.

## What happened in the opencode MCP OAuth flaw?

[Pillar Security reported](https://www.pillar.security/blog/open-and-shut-remote-code-execution-in-opencode-via-mcp-oauth) that opencode, an open-source terminal coding agent, took the `authorization_endpoint` from a remote MCP server's OAuth discovery metadata and passed it, unchecked, to the `open` npm package (version 10.1.2) to launch the browser.

On Windows, that version of `open` builds a PowerShell command and runs it with `-EncodedCommand`. It escaped the application name but not the target URL, and wrapped the URL in double quotes. PowerShell expands `$(...)` subexpressions inside double-quoted strings, so a URL containing one ran the embedded command before the browser launch even happened. macOS and Linux were not affected because their launchers do not go through a shell.

The key details, as reported by Pillar and summarized by the [Cloud Security Alliance](https://labs.cloudsecurityalliance.org/research/csa-research-note-mcp-launch-path-rce-opencode-copilot-20261/):

- **Affected:** opencode 1.0.137 through 1.18.32 on Windows and WSL. The [Mend vulnerability database entry WS-2026-0171](https://www.mend.io/vulnerability-database/WS-2026-0171/) also lists 2.0.0 through 2.0.17 as affected, fixed in 2.0.18.
- **Fixed:** 1.18.33, published September 28, 2026, which upgraded `open` to 11.0.4 and added an http/https check.
- **Disclosure:** reported privately on August 29, 2026. Pillar says Anomaly, the maintainer, described a malicious MCP server as outside its threat model and shipped the fix as a refactor. No CVE or advisory was issued.
- **Exploitation:** none reported in the wild.

Sources differ on interaction. CSA notes that Pillar describes no interaction beyond configuring the server, while The Hacker News reports that the browser step runs when a developer starts sign-in. Either way, the trigger is ordinary use of a configured server.

## Why didn't a URL scheme check stop it?

The payload was a well-formed `https://` URL. It contained no spaces, so URL parsing left it intact, and the `$(...)` sat in the path. A check that only asks "is this http or https?" passes it.

Pillar and CSA both make the same point: the scheme check added in the fix was not what closed the hole. The upgrade to `open` 11.0.4, which quotes the target differently, did. CSA's framing is useful for reviewers: the control checked one representation of the data (a parsed URL) while a different interpreter (PowerShell) executed another.

Compare the older `mcp-remote` bug. [JFrog's advisory for CVE-2025-6514](https://research.jfrog.com/vulnerabilities/mcp-remote-command-injection-rce-jfsa-2025-001290844/) (CVSS 9.6, versions 0.0.5 through 0.1.15) describes the same root cause, a crafted `authorization_endpoint` from an untrusted server, with a proof of concept using a `file:` URL to launch a local program. A scheme allowlist would have stopped that one. The opencode case shows why it is necessary but not sufficient.

> **No CVE means no scanner alert:**
>
> Because the opencode flaw has no CVE, dependency scanners and vulnerability feeds will not flag vulnerable installs. Check versions directly.

## What does the MCP specification require of clients?

The MCP [security best practices](https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices) include a section on OAuth authorization URL validation that describes both the `javascript:` URL and the shell-injection variants. For clients it says:

- Only `http://` and `https://` schemes are allowed for authorization URLs, and `http://` only for loopback during local development.
- `javascript:`, `data:`, `file:`, `vbscript:`, and similar schemes must be rejected, preferably with an allowlist.
- Clients must not use shell commands such as `cmd.exe`, `sh`, or PowerShell to open URLs, and should use platform-specific non-shell mechanisms.
- Clients must sanitize and validate all URLs received from MCP servers, and should log suspicious ones.

The same document covers SSRF during OAuth discovery, which is the server-side version of the same lesson: `resource_metadata`, `authorization_servers`, and `authorization_endpoint` all come from the server you are connecting to.

## How should an MCP client validate and open authorization URLs?

Treat the authorization URL the way you would treat any string from a remote party that ends up near a process launcher. Three layers, in order of importance:

1. **Never build a shell command from it.** Pass the URL as a single argument to a launcher that does not invoke a shell. This is the control that would have stopped both bugs.
2. **Bind it to an expected authorization server.** The authorization server may legitimately live on a different origin from the MCP server, so record the expected origin when the server is approved, and reject anything else.
3. **Allowlist scheme and characters.** Require `https:` (or `http:` on loopback in development), and reject characters that no legitimate authorization endpoint needs but shells interpret.

A minimal sketch in TypeScript for Node:

```ts
import { spawn } from "node:child_process";

// Characters shells treat specially. Legitimate authorization endpoints do not need them.
const SHELL_META = /[\s"'`$;&|<>(){}^!\\]/;
const LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]"]);

export function checkAuthorizationEndpoint(raw: string, approvedOrigins: Set<string>): URL {
  const url = new URL(raw); // throws on unparseable input
  const isHttps = url.protocol === "https:";
  const isDevLoopback = url.protocol === "http:" && LOOPBACK.has(url.hostname);
  if (!isHttps && !isDevLoopback) throw new Error("authorization URL must use https");
  if (!approvedOrigins.has(url.origin)) throw new Error("unapproved authorization server: " + url.origin);
  if (SHELL_META.test(raw) || SHELL_META.test(url.href)) throw new Error("unexpected characters in authorization URL");
  return url;
}

// Validate the endpoint first, then add your own query parameters,
// which URLSearchParams percent-encodes.
export function openInBrowser(url: URL): void {
  if (process.platform === "darwin" || process.platform === "linux") {
    const cmd = process.platform === "darwin" ? "open" : "xdg-open";
    spawn(cmd, [url.href], { shell: false, stdio: "ignore", detached: true }).unref();
    return;
  }
  // Elsewhere, print the URL for the user to open rather than shelling out.
  console.log("Open this URL to sign in:\n" + url.href);
}
```

Two notes on the sketch. The character check runs on the endpoint from metadata, before you append `client_id`, `state`, and PKCE parameters, so legitimate `&` separators are not affected. And the Windows fallback of printing the URL is a deliberately conservative choice; if you use a library to launch browsers on Windows, read what command it actually runs and pin a version you have checked, as the `open` 10 to 11 change shows.

For a broader take on why text from a remote party must be encoded for the sink it reaches, see [LLM output is untrusted input](https://blog.enforster.ai/llm-output-is-untrusted-input). The same rule applies here; only the source is different.

## What should teams using MCP clients do now?

If your developers use opencode or other MCP-capable agents on Windows or WSL:

- **Inventory versions directly.** Check installed opencode versions (for example, global npm listings for `opencode-ai`, or the tool's version output) and upgrade anything below 1.18.33, or below 2.0.18 on the 2.x line.
- **Review remote MCP servers.** Run `opencode mcp list` and remove remote servers nobody has vetted. Pillar recommends this as the stopgap where upgrading is not immediate. Our [MCP server review checklist](https://blog.enforster.ai/vetting-third-party-mcp-servers) covers what vetting should include.
- **Pin remote servers to known origins** and treat changes to MCP configuration like changes to CI pipeline definitions: reviewed and monitored.
- **Watch process trees.** CSA suggests alerting when an agent process (which may appear as `opencode.exe`, Bun, or Node.js) spawns `powershell.exe` with `-EncodedCommand`. These are heuristics, not published indicators of compromise.

> **Ask vendors the threat-model question:**
>
> When evaluating an AI coding tool, ask whether a malicious MCP server is inside its threat model, and whether it issues CVEs for flaws that require one. The answer tells you whether you will hear about the next bug of this kind.

## Why this keeps recurring in agent tooling

MCP clients sit in an unusual place: they run on developer machines with the developer's credentials, and they routinely consume metadata from servers chosen by whoever edited a config file. That includes a coding agent acting on injected instructions, as covered in [the AGENTS.md hijack](https://blog.enforster.ai/agents-md-instruction-file-hijack-coding-agents). Every field in that metadata, from tool descriptions to OAuth endpoints, is attacker-influenced if the server is.

The engineering rule is old and still sufficient: data from a remote party never becomes part of a command string. Pass it as an argument, validate it against what you expected, and assume the server on the other end may be hostile.
