Originally published at webofmike.com on 2026-10-21. The demo repo was run before publishing.
If your agent can pick a file path and your code decides whether that write is allowed, go look at the order of two lines. Check, then resolve, is the bug. Resolve, then check, is the fix.
That's the short version of CVE-2026-103572, a remote code execution bug in the Copilot component of Visual Studio and SQL Server Management Studio. It was disclosed by embracethered.com, rated CVSS 8.8, and patched by Microsoft on Patch Tuesday, 2026-10-13.
What went wrong
Copilot protected a list of its own config files, including the one that registers MCP servers. The guard compared the requested filename as a string. The write then went to whatever path the filesystem resolved. Those two things didn't always agree.
The writeup shows it breaking three ways:
-
Trailing dot. On Windows,
settings.json.andsettings.jsonend up as the same file, but they are different strings. The guard saw no match and let it through. -
Path traversal. A path that starts inside a trusted directory can still use
..to land outside it. - Prompt injection in the model driving the agent, which is how an attacker gets a bad path into a tool call in the first place.
Once a write reaches the MCP settings file, the rest is short. Add a server entry, point its launch command at something you control, and the IDE runs it the next time it loads MCP servers. The proof of concept opened calc.exe. The fix reportedly adds a GetFullPath call so the path is canonical before anything else looks at it.
The part that isn't Windows-specific
The trailing dot only works on Windows. The .. case works anywhere, and it's the same mistake: you validate one string and write to a different path.
I built a portable repro in themsquared/agent-config-guard-bypass. An agent may write inside workspace/. A sibling directory, agent_home/, holds mcp_settings.json, which decides which MCP servers get launched. The agent should never reach it.
The vulnerable guard:
def guard_write(requested_path: str, content: str) -> Path:
candidate = WORKSPACE / requested_path
# BUG: checked as a raw string, before ".." is resolved.
if not str(candidate).startswith(str(WORKSPACE)):
raise PermissionError(f"refused: {requested_path} escapes workspace")
resolved = candidate.resolve() # ".." collapses here, after the check
resolved.parent.mkdir(parents=True, exist_ok=True)
resolved.write_text(content)
return resolved
Ask it for ../agent_home/mcp_settings.json. Before resolution the string really does start with the workspace path, so the check passes. Then .resolve() collapses the .. and the write lands outside the workspace, in the one file that controls what gets launched.
The fixed guard moves one call:
def guard_write(requested_path: str, content: str) -> Path:
resolved = (WORKSPACE / requested_path).resolve() # resolve FIRST
if resolved != WORKSPACE and WORKSPACE not in resolved.parents:
raise PermissionError(f"refused: {requested_path} escapes workspace")
resolved.parent.mkdir(parents=True, exist_ok=True)
resolved.write_text(content)
return resolved
Now the check and the write use the same canonical path. There's no raw string left to craft against.
Run it
git clone https://github.com/themsquared/agent-config-guard-bypass.git
cd agent-config-guard-bypass
python3 exploit_demo.py
The vulnerable guard writes a new update-check server into mcp_settings.json. The fixed guard refuses the same call before anything is written.
Where else to look
This landed in an IDE, but the pattern shows up anywhere an LLM chooses a path and code acts on it: an agent harness's own tool handler, an MCP server's working-directory sandbox, a policy check in front of a set of MCP tools. Any of them can be one startswith() before resolve() away from the same hole. The fix needs no new dependency and no redesign.
What the repo doesn't cover: the trailing-dot variant (it needs Windows and NTFS) and the prompt-injection delivery, which is a model problem rather than a file-write problem. The original writeup is the place for both.
Full post, with an FAQ on the CVE and on why an MCP config file is an RCE target: webofmike.com/trailing-dot-mcp-config-bypass
Top comments (0)