critical

CVE

Not assigned

CWE

CWE-78, CWE-88, CWE-346, CWE-350

Affected Surface

  • npm package `opencode-ai` (OpenCode CLI and TUI) versions `1.0.137` through `1.18.32` on Windows and WSL when connecting or signing in to a remote MCP server (fixed in `1.18.33`, no CVE or vendor advisory issued)
  • npm package `@modelcontextprotocol/inspector` versions `2.0.0` through `2.11.0` on Windows and WSL via OAuth `authorization_endpoint` PowerShell subexpression evaluation (`GHSA-q978-78c7-phqw`, fixed in `2.12.0`)
  • npm packages `@modelcontextprotocol/inspector` and `@modelcontextprotocol/inspector-client` versions `0.13.0` through `1.0.2` on all operating systems via OAuth callback `transport=stdio` parameter injection (`GHSA-4fc4-wcmh-4g86`, fixed in `1.0.3`)
  • npm packages `@modelcontextprotocol/inspector` and `@modelcontextprotocol/inspector-server` versions `0.14.1` through `1.0.2` and `2.0.0` through `2.11.0` across companion 21 October advisories (`GHSA-vcc9-3xgm-p9v4`, `GHSA-wmjf-2m46-2352`, `GHSA-f6mc-jg3x-vq53`, `GHSA-m5pj-3536-3xcp`)
  • Any Node.js CLI or agent tool on Windows or WSL that passes a remote peer's URL to `open` `< 11.0.0`, even when guarded by an `http:` / `https:` protocol check

TL;DR

Connecting an AI coding agent or MCP debugging client to a remote Model Context Protocol (MCP) server over HTTP does not just expose the model to untrusted tool output. Before any tool is listed or any prompt is sent, an HTTP MCP server can answer 401 Unauthorized and hand the client an OAuth discovery document whose authorization_endpoint is an attacker-chosen string.

Between 19 and 22 October 2026, Pillar Security and the maintainers of @modelcontextprotocol/inspector disclosed that two widely used MCP developer tools passed that authorization_endpoint straight into open@10.x.

In OpenCode (opencode-ai on npm, ~3 million weekly downloads), versions 1.0.137 through 1.18.32 on Windows and WSL execute arbitrary PowerShell commands embedded as $(...) inside an ordinary https:// authorization URL when a developer runs opencode mcp auth or signs in to a remote MCP server. Anomaly fixed the flaw in 1.18.33 (commit 29f07e0c) by upgrading open from 10.1.2 to 11.0.4, but shipped the change as a browser-opener refactor without a CVE or security advisory. Standard SCA scanners will not alert on vulnerable installs.

In MCP Inspector (@modelcontextprotocol/inspector on npm), the maintainers published six security advisories on 21 October 2026, fixed in 2.12.0 (for the v2 line) and 1.0.3 (for the v1 line). Two give a remote MCP server immediate code execution as the developer: GHSA-q978-78c7-phqw (CVSS 8.8) is the same open@10.2.0 PowerShell subexpression injection on Windows and WSL, and GHSA-4fc4-wcmh-4g86 (CVSS 9.6) lets an OAuth /authorize redirect append ?transport=stdio&serverCommand=... to /oauth/callback, switching a remote connection into a local stdio process spawn on macOS, Linux, and Windows.

Upgrade opencode-ai to 1.18.33 or later (1.19.4 is current) and @modelcontextprotocol/inspector to 2.12.0 or 1.0.3. Because OpenCode has no CVE for this flaw, grep lockfiles and check developer workstations directly with opencode --version.

Affected packages and projects

EcosystemPackageAdvisory / identifierAffected versionsFixed or safe versionPlatform / condition
npmopencode-aiNo CVE / no GHSA (Pillar Security report, commit 29f07e0c)>= 1.0.137, <= 1.18.321.18.33 (1.19.4 latest)Windows and WSL when initiating OAuth sign-in to a remote MCP server
npm@modelcontextprotocol/inspectorGHSA-q978-78c7-phqw (CVSS 8.8)>= 2.0.0, <= 2.11.02.12.0Windows and WSL when connecting to a remote MCP server that returns 401 OAuth discovery
npm@modelcontextprotocol/inspector, @modelcontextprotocol/inspector-clientGHSA-4fc4-wcmh-4g86 (CVSS 9.6)>= 0.13.0, <= 1.0.21.0.3 (2.12.0 on v2)All OSes when completing OAuth redirect to /oauth/callback
npm@modelcontextprotocol/inspectorGHSA-vcc9-3xgm-p9v4 (High)>= 2.0.0, <= 2.11.02.12.0All OSes; DNS rebinding steals window.__INSPECTOR_API_TOKEN__ and /api/storage/oauth secrets
npm@modelcontextprotocol/inspector, @modelcontextprotocol/inspector-serverGHSA-wmjf-2m46-2352 (CVSS 7.5)>= 0.14.1, <= 1.0.2 and 2.0.0 to 2.11.01.0.3, 2.12.0All OSes when started with DANGEROUSLY_OMIT_AUTH=true; absent Origin header spawns local stdio processes
npm@modelcontextprotocol/inspector, @modelcontextprotocol/inspector-serverGHSA-f6mc-jg3x-vq53 (Medium)>= 1.0.0, <= 1.0.21.0.3All OSes; /fetch manual redirect loop leaks Authorization and Cookie headers cross-origin
npm@modelcontextprotocol/inspector, @modelcontextprotocol/inspector-serverGHSA-m5pj-3536-3xcp (Medium)>= 0.21.2, <= 1.0.2 and >= 2.0.0, <= 2.11.01.0.3, 2.12.0All OSes; unbounded await res.text() in /api/fetch and /fetch allows memory exhaustion
npmopen (transitive sink)Upstream commit 274d704 (issues #202, #318, #319, #323)< 11.0.0 (when caller passes untrusted URLs)11.0.0 (11.0.4 latest)Windows (win32) and WSL outside containers

Technical analysis

1. Why https:// scheme validation fails against open@10.x on Windows and WSL

After CVE-2025-6514 hit mcp-remote in July 2025, many MCP clients and SDKs treated browser-launch command injection as a URL-scheme problem: reject javascript:, data:, file:, or non-HTTP schemes, allow http: and https:, and pass url.href to open().

Both Pillar Security’s report on OpenCode and GHSA-q978-78c7-phqw in MCP Inspector show why that control fails when the underlying opener is open 10.x (10.1.2 in OpenCode 1.18.32, 10.2.0 in MCP Inspector 2.11.0).

On macOS and native Linux, open launches open or xdg-open through child_process.spawn() with an argument array, so no shell interprets the URL. On Windows (process.platform === 'win32') and on WSL outside a container, open 10.2.0 (index.js lines 188 to 204) builds a PowerShell Start command, base64-encodes it as UTF-16LE, and runs powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand:

// sindresorhus/open v10.2.0 index.js
if (app) {
	encodedArguments.push(`"\`"${app}\`""`);
}
...
} else if (options.target) {
	encodedArguments.push(`"${options.target}"`);
}

options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');

Notice the asymmetry between those two branches. The app binary path has its inner quotes backtick-escaped (\”), while options.target` is dropped directly inside double quotes:

Start "https://evil.test/$([IO.File]::WriteAllText('C:\Users\Public\pwned.txt','RCE'))"

In PowerShell, double-quoted strings are expandable strings. The parser evaluates every $(...) subexpression before it invokes Start. Even better for an attacker, WHATWG URL parsing (new URL(input).href) percent-encodes spaces and double quotes in the path, query, and fragment, but leaves every character needed for a PowerShell method call untouched:

  • Preserved in path, query, and fragment: $, (, ), [, ], :, ,, +, &
  • Preserved in path and fragment: ' (single quote)

Because [IO.File]::WriteAllText('C:\Users\Public\pwned.txt','RCE') contains no spaces and no double quotes, new URL() parses it as a valid https: URL and serializes it unchanged. Every scheme filter waves it through:

  1. The MCP TypeScript SDK’s client denylist (blocking javascript:, data:, and vbscript:) allows it.
  2. OpenCode’s new openUrl() check (url.protocol !== "http:" && url.protocol !== "https:") allows it.
  3. MCP Inspector’s OPENABLE_PROTOCOLS allowlist (new Set(["http:", "https:"])) allows it.

In OpenCode 1.18.32, the path from the remote server’s JSON response to PowerShell ran through packages/opencode/src/mcp/oauth-provider.ts and packages/opencode/src/mcp/browser.ts. In MCP Inspector 2.11.0, the same discovery field flowed from core/auth/discovery.ts and core/auth/providers.ts into clients/cli/src/cli-oauth-navigation.ts and three separate openUrl() helpers (clients/cli/src/open-url.ts, clients/tui/src/utils/openUrl.ts, and clients/web/server/web-server-config.ts):

// modelcontextprotocol/inspector @ edf54f5de: clients/cli/src/open-url.ts
export async function openUrl(url: string | URL): Promise<void> {
  await open(typeof url === "string" ? url : url.href);
}

In MCP Inspector’s CLI, auto-opening the browser is enabled whenever VITEST is unset and MCP_AUTO_OPEN_ENABLED is not "false". Pointing npx @modelcontextprotocol/inspector --cli at an untrusted server on Windows or WSL fires the chain on the first 401 response without a prompt.

2. How open@11.0.4, OpenCode 1.18.33, and MCP Inspector 2.12.0 close the sink

Upstream sindresorhus/open fixed the PowerShell target escaping in version 11.0.0 (commit 274d704, November 2025) by wrapping arguments in single quotes via executePowerShell.escapeArgument(options.target) and doubling any embedded single quotes. PowerShell treats single-quoted strings as verbatim literals and does not evaluate $(...).

OpenCode’s fix in PR #51414 (commit 29f07e0c, released in 1.18.33) upgrades open from 10.1.2 to 11.0.4, centralizes browser launches in packages/core/src/open.ts, and checks the protocol in both openUrl() and McpOAuthProvider.redirectToAuthorization():

--- a/packages/opencode/src/mcp/oauth-provider.ts
+++ b/packages/opencode/src/mcp/oauth-provider.ts
@@ -125,6 +125,10 @@ export class McpOAuthProvider implements OAuthClientProvider {
   async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
+    if (authorizationUrl.protocol !== "http:" && authorizationUrl.protocol !== "https:")
+      throw new Error(
+        `MCP server "${this.serverUrl}" returned a ${authorizationUrl.protocol} authorization URL; only http and https are supported`,
+      )
     await this.callbacks.onRedirect(authorizationUrl)
   }

MCP Inspector 2.12.0 (commit 73f3cf8e23) likewise upgrades open from ^10.2.0 to ^11.0.4 across the root workspace and clients/web, and rewrites core/node/openUrl.ts so the caller always opens the normalized parsed.href returned by new URL(href) rather than the raw input string:

// modelcontextprotocol/inspector commit 73f3cf8e23: core/node/openUrl.ts
const OPENABLE_PROTOCOLS: ReadonlySet<string> = new Set(["http:", "https:"]);

function toOpenableHref(href: string): string {
  let parsed: URL;
  try {
    parsed = new URL(href);
  } catch {
    throw new Error("refusing to open an invalid URL");
  }
  if (!OPENABLE_PROTOCOLS.has(parsed.protocol)) {
    throw new Error(`refusing to open a non-http(s) URL (${parsed.protocol})`);
  }
  return parsed.href;
}

3. GHSA-4fc4-wcmh-4g86: OAuth callback query pollution switches remote HTTP to local stdio RCE

While GHSA-q978-78c7-phqw only affects Windows and WSL, GHSA-4fc4-wcmh-4g86 (CVSS 9.6 Critical) hits @modelcontextprotocol/inspector 0.13.0 through 1.0.2 on every operating system.

When a developer connects MCP Inspector to a remote server that requires OAuth, the remote authorization server redirects the user’s browser back to Inspector’s local callback route (http://127.0.0.1:6274/oauth/callback). An attacker-controlled authorization server can preserve the legitimate code and state parameters while appending three Inspector UI query parameters to that redirect:

http://127.0.0.1:6274/oauth/callback?code=evil-code&state=<valid-state>&transport=stdio&serverCommand=calc.exe&serverArgs=

When /oauth/callback loads in the browser, client/src/App.tsx initializes the React state for the connection directly from window.location.search:

// Vulnerable initialization in client/src/App.tsx (<= 1.0.2)
const [command, setCommand] = useState<string>(getInitialCommand);
const [args, setArgs] = useState<string>(getInitialArgs);
const [transportType, setTransportType] = useState<
  "stdio" | "sse" | "streamable-http"
>(getInitialTransportType);

OAuthCallback.tsx then extracts params.code, finishes the OAuth token exchange, and calls onConnect(serverUrl) to reconnect automatically. Because transportType was overwritten with "stdio" and command was overwritten with serverCommand from the callback URL, connectMcpServer() sends an authenticated request (carrying the browser’s valid proxy session token) to the local proxy’s /stdio endpoint, which spawns StdioClientTransport with the attacker’s command and arguments.

The v1.0.3 fix (commit 0d5c0469b1) stops reading connection parameters from the URL on callback routes and snapshots the active connection in per-tab sessionStorage before redirecting:

--- a/client/src/utils/configUtils.ts
+++ b/client/src/utils/configUtils.ts
@@ -3,8 +3,15 @@
+import { isOAuthCallbackPath } from "@/lib/oauthConnectionSnapshot";
+
 const getSearchParam = (key: string): string | null => {
+  if (isOAuthCallbackPath()) return null;
   try {
     const url = new URL(window.location.href);
     return url.searchParams.get(key);

4. DNS rebinding and proxy bypasses in the 21 October MCP Inspector batch

Four other advisories in the 21 October MCP Inspector release close gaps in the local web server and proxy that sit between the browser and the developer’s machine.

Under GHSA-vcc9-3xgm-p9v4 (affecting 2.0.0 through 2.11.0), the v2 production Hono server injected window.__INSPECTOR_API_TOKEN__ into the HTML returned by GET / and the SPA fallback without checking the Host header, while originValidationMiddleware allowed requests with no Origin header (which browsers omit on same-origin GET requests). After DNS rebinding evil[.]com:6274 to 127.0.0.1, attacker JavaScript could fetch("http://evil[.]com:6274/") to read the API token and then call GET /api/storage/oauth and GET /api/import-source to read stored OAuth tokens and local config files. Version 2.12.0 (commit 123998321d) fixes this by placing hostAllowListMiddleware in front of every route except /healthz.

Under GHSA-wmjf-2m46-2352, setting DANGEROUSLY_OMIT_AUTH=true disabled token checks while the remaining Origin middleware still allowed requests with no Origin header, so any local or network-reachable non-browser client could POST /api/mcp/connect (v2) or GET /stdio (v1) to spawn commands. Versions 1.0.3 and 2.12.0 (commit e9cd45b987) require an allowed Origin header whenever auth is disabled and restrict DANGEROUSLY_* flags to explicit true, 1, or yes values.

Finally, GHSA-f6mc-jg3x-vq53 and GHSA-m5pj-3536-3xcp cover the /fetch and /api/fetch proxy routes. A manual redirect loop added for SSRF protection in 1.0.0 reused the initial headers object on every hop, forwarding Authorization and Cookie headers across cross-origin 301/302 redirects (commit b2e41c1680 strips sensitive headers on cross-origin or scheme-downgrading hops). Both /fetch (v1) and POST /api/fetch (v2) also buffered upstream responses with an uncapped await res.text(), fixed in commit 4862b74609 by enforcing a streaming byte budget (readCappedText).

Detection and remediation

1. Check installed OpenCode and MCP Inspector versions

Because Anomaly did not assign a CVE or publish a GHSA for the OpenCode MCP OAuth bug, dependency scanners will not flag opencode-ai 1.0.137 through 1.18.32. Check developer machines, WSL distributions, and project lockfiles directly:

# Check global or active OpenCode CLI version
opencode --version 2>/dev/null || true

# List configured MCP servers in OpenCode
opencode mcp list 2>/dev/null || true

# Search lockfiles and package manifests for vulnerable opencode-ai and @modelcontextprotocol/inspector versions
rg -n '"(opencode-ai|@modelcontextprotocol/inspector(-(client|server))?)"\s*:\s*"[^"]*"' \
  package.json package-lock.json pnpm-lock.yaml yarn.lock bun.lock 2>/dev/null

Also check whether npx caches on developer workstations hold older @modelcontextprotocol/inspector versions (<= 2.11.0 or <= 1.0.2), since developers commonly launch Inspector via an unpinned npx @modelcontextprotocol/inspector:

find ~/.npm/_npx -name 'package.json' -exec grep -H '"@modelcontextprotocol/inspector"' {} + 2>/dev/null

2. Audit your own code for open < 11.0.0 on remote URLs

Any Node.js CLI, agent, or desktop wrapper that passes a server-supplied URL to open < 11.0.0 on Windows or WSL has the same PowerShell subexpression sink, even if it validates parsed.protocol === "https:":

npm ls open 2>/dev/null || pnpm why open 2>/dev/null || true

If your code opens URLs received from remote servers (such as OAuth authorization_endpoint, OIDC discovery metadata, or webhook links):

  1. Upgrade open to ^11.0.4, which single-quotes targets in executePowerShell.escapeArgument().
  2. Parse the input with new URL(), enforce an https: (or loopback http:) allowlist, and pass the serialized parsed.href rather than the raw input string.
  3. Where possible, avoid shell-backed launchers altogether or percent-encode $ before handing the URL to an external opener.

3. Upgrade and rotate credentials if exposed

  1. Upgrade opencode-ai to 1.18.33 or later (npm install -g opencode-ai@latest) on every Windows and WSL workstation. If you cannot upgrade immediately, remove unvetted remote HTTP MCP servers from your OpenCode configuration (~/.config/opencode/ or project .opencode/).
  2. Upgrade @modelcontextprotocol/inspector to 2.12.0 (or 1.0.3 if pinned to the v1 line), clear stale ~/.npm/_npx cache entries, and stop running Inspector with DANGEROUSLY_OMIT_AUTH=true.
  3. If a developer connected OpenCode (<= 1.18.32 on Windows/WSL) or MCP Inspector (<= 2.11.0 / <= 1.0.2) to an untrusted or unexpected remote MCP server, audit Windows PowerShell operational logs (Event ID 4104 / 4688 for powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand) and rotate any OAuth tokens, cloud credentials, and API keys stored on that workstation.

Timeline

  • 2020-10-22: sindresorhus/open issue #202 reports that URLs containing $ break on WSL 2 because PowerShell expands $ inside double quotes.
  • 2023-10-18: sindresorhus/open issue #323 demonstrates open('https://$(calc.exe)') as OS command injection on Windows.
  • 2025-07-09: CVE-2025-6514 (CVSS 9.6) is disclosed in mcp-remote for the same OAuth authorization_endpoint to open() sink.
  • 2025-11-15: open 11.0.0 (commit 274d704) ships single-quoted PowerShell argument escaping via executePowerShell.escapeArgument().
  • 2025-12-07: OpenCode adds MCP OAuth browser launching while pinned to open@10.1.2.
  • 2026-08-29: Pillar Security privately reports the Windows/WSL MCP OAuth RCE to Anomaly via a GitHub Security Advisory.
  • 2026-09-25: Anomaly merges PR #51414 (commit 29f07e0c), upgrading open to 11.0.4 and adding openUrl() scheme checks, while stating that malicious remote MCP servers are outside its threat model.
  • 2026-09-28: OpenCode 1.18.33 is published to npm without a security advisory or CVE.
  • 2026-10-19 to 2026-10-22: Pillar Security publishes its technical disclosure and The Hacker News and Infosecurity Magazine report on the unindexed flaw.
  • 2026-10-21: @modelcontextprotocol/inspector releases versions 2.12.0 and 1.0.3 and publishes six security advisories (GHSA-q978-78c7-phqw, GHSA-4fc4-wcmh-4g86, GHSA-vcc9-3xgm-p9v4, GHSA-wmjf-2m46-2352, GHSA-f6mc-jg3x-vq53, and GHSA-m5pj-3536-3xcp).

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References