MCP Inspector's OAuth flow could run PowerShell on Windows: inside CVE-2026-119025, and why npm update misses the fix
The MCP Inspector is the official debugging tool for the Model Context Protocol, and it is the tool AWS, Cloudflare and Azure docs point at when you need to test a remote MCP server before trusting it. On November 5, Pil
The MCP Inspector is the official debugging tool for the Model Context Protocol, and it is the tool AWS, Cloudflare and Azure docs point at when you need to test a remote MCP server before trusting it. On November 5, Pillar Security published the uncomfortable punchline: on Windows and WSL, the Inspector itself let a remote MCP server run code on your machine during the OAuth handshake. No tool call, no approval prompt, no model involved. The advisory (GHSA-q978-78c7-phqw, CVSS 3.1 8.8) went up October 21 and the CVE record (CVE-2026-119025, CVSS 4.0 7.7) landed October 29, but the Inspector-specific walkthrough is days old, and it is worth understanding if you ever test MCP servers on a Windows machine.
The tool you vet servers with was the attack surface
The Inspector's whole purpose is to be pointed at servers whose behavior you do not yet trust. That is what it is for. Pillar puts it at about 768,000 weekly npm downloads, and the exposed population is exactly the people deliberately connecting to unknown servers, on the operating system where this bug bites. The irony is not subtle. You run the Inspector to avoid giving an untrusted server access to your machine, and the act of running it did the opposite.
The chain, step by step
The bug needs five moves, and none of them involve the model:
- You point the Inspector at a remote MCP server. It answers
401with aWWW-Authenticatechallenge and serves OAuth discovery documents. - The
authorization_endpointinside those documents is a free-form string chosen entirely by that server. - The Inspector opens that URL automatically through npm's
openpackage. Auto-open is on by default in an interactive terminal, and the same call existed in three places: the CLI, the TUI and the web client. - On Windows and WSL,
open10.x places the URL inside a PowerShell double-quoted string, base64-encodes the result and runs it with-EncodedCommand. Nothing escapes the URL. - PowerShell evaluates
$(..)subexpressions inside double quotes. The payload is an ordinary, valid https URL:
https://evil.test/$([IO.File]::WriteAllText('C:\Users\Public\pwned.txt','RCE'))
URL serialization percent-encodes spaces and double quotes but preserves $ ( ) [ ] : , + &, so the payload needs no encoded character at all. The code runs while you are still connecting, before a single tool is listed.
Why the usual defenses do not fire
Three details make this worse than it first looks.
It is a valid https URL. A scheme check, the defense several projects adopted for this exact class of bug, passes it cleanly. What actually stops this payload is open 11, which escapes the argument and places it in a single-quoted PowerShell string, where $(..) is literal text. Scheme validation is defense in depth here, not the fix.
The model plays no part. Nothing is approved and no tool is invoked, so permission rules in your agent config never see it. I write about agent guardrails a lot, and this is the case they cannot cover: the injection happens one layer below the agent, in the connect path.
The server did not have to be malicious when you added it. The payload arrives in a discovery response at request time, so a typosquatted package name, an expired domain or a later compromise all produce the same result.
Honest limits: the researcher demonstrated the sink string executing in PowerShell, while the Windows and WSL reachability is established by code review of the win32 branch. No in-the-wild exploitation has been reported.
Who is affected, exactly
- Windows and WSL, for this chain. On macOS and Linux,
opencallsxdg-openoropenthroughspawnwith an argument vector, no shell, so this particular path does not exist there. - Component ranges from the CVE record:
open>= 7.2.0 and < 11.0.0,@modelcontextprotocol/inspector>= 2.0.0 and < 2.12.0, andopencode-ai>= 1.0.137 and < 1.18.33, which carried the same chain. - The v1 Inspector line had a nastier sibling: GHSA-4fc4-wcmh-4g86, rated 9.6, let an OAuth callback's query parameters flip a remote session into local
stdiocommand execution on any OS. That one is fixed in 1.0.3.
The fix npm update will not apply
All three Inspector dependency manifests pinned open as a caret range:
"open": "^10.2.0"
A caret range never crosses a major version, so 10.x stays 10.x no matter how many times you run npm update. The maintainers had to bump deliberately to open 11.0.4 and add an overrides entry so the web client's transitive copy could not linger on 10.x. If your own tooling hands a remote peer's URL to open(), the same trap is waiting in your lockfile. Check what actually resolved:
npm ls open @modelcontextprotocol/inspector
Manifest intent is not installed truth. This is the same discipline I keep hitting in agent configs, where a version pin or an allowlist silently drifts from what runs in production.
It did not stop at 2.12.0
On November 4 the Inspector shipped 2.14.0 with three more fixes, all affecting 2.13.0 and below (summary): a sandboxed MCP App frame could navigate away from the host's policy while the proxy kept relaying; importing a server.json could splice unvalidated runtime arguments into npx, uvx or docker run while the UI showed only the benign package name; and a server-supplied outputSchema.pattern could hang the UI with a ReDoS after a tool call. If your workflow is "point the Inspector at the weird server and watch", that middle one reads as a direct message to you.
How I treat the tool now
The lesson generalizes past this CVE. Every tool in the connect-and-observe path is attack surface, including the observer. Positron shipping IDE-wide MCP guardrails was the same realization at the IDE layer: the seat between you and the unknown server needs its own policy. When agents started picking deprecated MCP servers on their own, the answer was guardrails around the agent. This is the same problem one layer down, and the answer is the same shape.
If you vet unknown MCP servers:
- Run the Inspector in a container or a throwaway VM, not on your daily Windows account. The chain needs the PowerShell branch, and a Linux container on a Windows host removes it.
- Pin deliberately. A caret range on a package whose job is to touch the OS, like opening a browser or spawning a command, is a standing bet that its next major version does not matter. It sometimes does.
- Re-grep the lockfile after news like this, not only during audits.
opensits deep in a lot of dependency trees.
What to check today
- Inspector at 2.12.0 or later, 2.14.0 if you import
server.jsonfiles. opencode at 1.18.33 or later. Any directopendependency at 11.0.0 or later. - On Windows, if you connected the Inspector to servers you no longer fully trust during October, treat it like any untrusted-code exposure: review scheduled tasks, run keys and approved SSH keys. The demo payload writes a file as your user, and a real one can do anything you can.
- On macOS and Linux you are clear of this particular chain, but check which Inspector line you run, because the v1 callback bug was cross-platform.
The protocol did not break here. A package boundary did. MCP's whole security model assumes the server may be hostile, and the Inspector's docs tell you to test untrusted servers with it. The tool that implements that advice has to be able to survive the advice, and for three weeks in October, on two platforms, it could not.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.