modelcontextprotocol / modelcontextprotocol/csharp-sdk
Document the Windows `cmd.exe /c` interposition as a security-relevant property, not just a quoting bug
@jozkee is already working on this.
Since Jul 31, 2026.
- Dominant language
- C#
- Stars
- 4.5k
- Forks
- 814
- Avg merge
- 9d 19h
- Merged PRs (30d)
- 4
Description
Summary
StdioClientTransport accepts Arguments as a string array, which is the standard .NET signal that arguments are passed to the child process as an argv vector without shell interpretation. On Windows that is not what happens: every non-cmd.exe command is re-wrapped as cmd.exe /c <command> <args…>, so a shell does parse the arguments.
This is well known as a functional bug — #1601 (open) covers the space-in-path failure and quotes the same code; #594 covered the ampersand failure. This issue is about the part neither raises: the wrapping is undocumented on the public API surface, and callers who reason about argument safety from the array-shaped API reach a conclusion that is false on Windows.
I am not asking for a functional fix here — #1601 already proposes good ones, and its option (b) would resolve this too. I am asking for the behaviour to be documented, because until it is fixed, correctness of the caller's security reasoning depends on knowing it.
The code
src/ModelContextProtocol.Core/Client/StdioClientTransport.cs, ConnectAsync (verified against the released ModelContextProtocol.Core 1.4.0 by decompilation, and matching main):
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
!string.Equals(Path.GetFileName(command), "cmd.exe", StringComparison.OrdinalIgnoreCase))
{
arguments = arguments is null or [] ? ["/c", command] : ["/c", command, ..arguments];
command = "cmd.exe";
}
The SDK then applies its own caret-escaping (EscapeArgumentString, pattern [&^><|]) to each element.
Why the documentation gap has consequences
A caller who builds Arguments as an array and never joins or splits it will reasonably conclude that no shell re-parses their argv, and will therefore treat shell metacharacters in argument values as inert. On Windows that conclusion is wrong, one layer below their own code. Concretely, this changes whether an input allowlist is defense-in-depth or load-bearing — and that difference determines whether it is safe for a future maintainer to relax it.
We hit exactly this. In an MCP governance gateway (darvoza) we had concluded that launching node npx-cli.js … rather than npx.cmd removed the shell from the launch path. It did not — it removed the batch file's own re-parse, while the SDK's cmd.exe /c remained. That conclusion sat in our security notes and code comments until we decompiled the package. No untrusted input reached argv in our case, so nothing was exploitable, but the reasoning was invalid and a comment told the next maintainer it was safe to relax the one guard that was actually carrying the weight.
The escaping helper covers [&^><|]. % (delayed/immediate variable expansion), ", (, ) and ; are not in that set. Whether that matters depends entirely on caller-side guarantees the SDK cannot see — which is the argument for saying so out loud.
What would help
In rough order of value:
- XML doc on
StdioClientTransportOptions.ArgumentsandCommandstating that on Windows the command is executed viacmd.exe /c, that the SDK applies[&^><|]escaping, and that callers should not assume argv reaches the child unparsed. - A short note in the README / client docs — a sentence where callers are told how to configure a stdio server is worth more than a source comment they will never open.
- If #1601's option (b) lands (skip the wrapping when
Commandresolves to a real executable), this narrows to PATH-resolved script commands and the docs can say so instead. That would be the better outcome; documenting is the interim.
The existing source comment justifies the wrapping as "usually npx or uvicorn", which reads as a PATH-resolution convenience. That intent is reasonable — it just does not appear to have been evaluated for what it implies about argument handling, and it is unconditional rather than scoped to the case that motivated it.
Environment
ModelContextProtocol/ModelContextProtocol.Core1.4.0- .NET 10, Windows 11
- Behaviour is Windows-only; on Linux/macOS the SDK spawns directly and the array-argv assumption holds.
Related
- #1601 — same wrapping, functional failure with spaces in
Command; proposes fixes that would also resolve this. - #594 — same wrapping, functional failure with
&in an argument.
Corroboration without a decompiler
For anyone verifying this from the package alone: the shipped ModelContextProtocol.Core.xml documents
StdioClientTransport.GetWindowsCliSpecialArgumentsRegex with pattern [&^><|]. A Windows CLI argument
escaper, living inside the stdio client transport, only makes sense if that transport builds a Windows
shell command line — which is the behaviour above. The 1.4.0 assembly also contains exactly one cmd.exe
string literal, consistent with the single assignment in the snippet.
To be clear about credit
This behaviour is not a discovery of ours — #594 reported it in 2025 and #1601 is open on it now. What
we are contributing is the argument-handling consequence neither thread raises, and the request to write it
down on the public API surface until the wrapping itself changes.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.