I found this issue while reviewing Visual Studio Code's MCP UI with a very specific security question in mind:
Are MCP server descriptions treated as plain display text, or can attacker-controlled metadata cross into a trusted markdown surface that is allowed to invoke internal command: links?
In the vulnerable builds, the answer was yes.
The MCP server hover UI created a trusted markdown container with isTrusted: true, then appended attacker-controlled server description content into it with appendMarkdown().
That mattered because VS Code's markdown pipeline already draws a hard security line around command: links:
That meant untrusted MCP metadata could become a click-triggered product action surface inside one of the most widely used developer tools in the world.
Microsoft later validated the report as a real security issue and classified it as Security Feature Bypass with Moderate Severity / Moderate Impact.
Affected product reviewed: Visual Studio Code
Reviewed vulnerable commit: 7a43958ba42a5fcb5455f068c5a0a7e322dc4532
VS Code is used by developers, startups, enterprises, and engineering teams worldwide. Microsoft reports that Visual Studio and VS Code together are actively used by 50 million developers every month.
attacker-controlled MCP server description -> VS Code hover builds MarkdownString({ isTrusted: true }) -> description appended with appendMarkdown() -> markdown renderer preserves command: links because the markdown is trusted -> user clicks hover link -> openerService.open(... allowCommands: true) -> built-in VS Code command executes
This issue was not limited to a local development snapshot. I later verified the public release boundary as well.
In those builds, the MCP hover description path still used:
isTrusted: trueappendMarkdown($)In those builds, the description path had already been hardened to:
isTrusted: falseescapeMarkdownSyntaxTokens(this.mcpServer.description)As of July 13, 2026, the current upstream main branch is also fixed.
So the clean public boundary is:
VS Code's MCP workbench UI exposes server metadata such as:
That is normal product behavior.
The issue was not that VS Code showed MCP descriptions. The issue was how it showed them.
MCP server descriptions are metadata. They are not product-authored trusted UI strings. They are not safe simply because they arrive through a structured manifest or gallery response.
That distinction matters even more in VS Code because the product already has an internal command: URI mechanism that can trigger privileged built-in actions.
Once untrusted text reaches a globally trusted markdown surface, the issue stops being presentation and becomes a real trust-boundary bug.
Rich markdown surfaces are always worth reviewing in a modern developer tool.
Especially when all of these are true:
That combination is exactly where small trust mistakes become real vulnerabilities.
The key question was not:
"Can an MCP description contain markdown?"
The real question was:
"Can an MCP description be rendered in a context trusted enough to preserve and activate
command:links?"
That was the right question.
The security boundary here was simple:
In the vulnerable path, those two trust levels were collapsed together.
The first important piece was the workbench-side getter in src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts:
That matters because it unified:
So once a malicious description entered the MCP server model, the hover path used it without any trust downgrade.
This was a caller-side trust bug, not a markdown renderer bug.
description as attacker-controlled textIn src/vs/platform/mcp/common/mcpGalleryService.ts, the MCP gallery parser accepted server descriptions as plain strings:
There was type validation. There was no escaping and no trust downgrade.
That is fine on its own. The bug showed up later, when the same string was upgraded into a trusted rendering context.
The same description was copied into local install metadata in src/vs/platform/mcp/common/mcpManagementService.ts:
So this was not just a transient network string. The metadata could survive into the locally managed MCP server representation.
The vulnerable sink lived in src/vs/workbench/contrib/mcp/browser/mcpServerWidgets.ts:
That is the bug in one place.
The description was not:
It was appended as markdown into a globally trusted markdown surface.
command: linksIn src/vs/base/browser/markdownRenderer.ts, VS Code already had a protection rule:
This is important because it shows the renderer already knew command: links should be blocked for untrusted markdown.
The vulnerability existed because the caller incorrectly set isTrusted: true, which disabled that protection for untrusted MCP data.
The next stage lived in src/vs/platform/markdown/browser/markdownRenderer.ts:
And the trust mapping was explicit:
So the chain was clean:
command: linksThat is a complete trust-boundary failure.
This was not a cosmetic markdown bug. It was not just "a link rendered."
The real issue was that untrusted server metadata was upgraded into a privileged UI action surface.
There is a major difference between:
The first is display behavior. The second is a security capability.
That distinction is exactly why VS Code's renderer had a dedicated command: trust gate in the first place.
It also matches the MCP security model. The MCP specification is explicit that descriptions and similar metadata should be treated as untrusted unless the consumer has a reason to trust them.
This path did the opposite.
I started with a deliberately simple PoC because the root issue did not need anything exotic.
The smallest useful payload was:
That proves the real claim without noise:
command: URIAt the gallery layer, the only field that mattered for this path was server.description.
A minimal malicious record looked like this:
In practice a real registry object can carry more metadata, but for this vulnerability the interesting requirements were simply:
server.nameserver.descriptionserver.versionI validated the core issue against vulnerable code carrying the trusted-hover pattern:
The reproduction was straightforward:
description contains a markdown command: link.getHoverMarkdown().The built-in VS Code command executed.
In the minimal payload above, the click opened the targeted settings page, which established the core security claim:
command: URI was preserved and activatedThat is already a valid security bug.
The minimal PoC mattered because it proved the primitive, not just a flashy side effect.
If I had started with a noisier payload, the report could have gotten stuck arguing about the specific command target rather than the actual trust failure.
The stronger first proof was:
command: linksThat is the vulnerability.
Everything else is capability exploration on top of that primitive.
Once the primitive exists, the next question is:
Which built-in commands are reachable from attacker-controlled metadata, and what do they do with attacker-controlled arguments?
That is where the issue becomes much more serious.
The two strongest follow-on paths I analyzed were:
These should be understood as impact expansion on top of the already validated command: execution primitive.
VS Code registers a built-in extension-install command in src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts:
That means a malicious MCP description can carry a payload like:
Decoded, the command arguments are:
That is materially stronger than a harmless settings pop:
Depending on environment state, this can lead into install confirmation or direct workflow execution. The important point is simpler:
untrusted server metadata should never have been able to reach this product action surface at all
The more serious environment-dependent path was workbench.action.terminal.sendSequence.
VS Code's own contribution in src/vs/workbench/contrib/terminalContrib/sendSequence/browser/terminal.sendSequence.contribution.ts accepts attacker-supplied text:
Its command metadata explicitly allows a { text: string } argument object.
That means a trusted markdown payload can target it directly:
Decoded, the argument is:
If an active terminal instance exists, that command path can send attacker-controlled text to the shell and include \r to submit it immediately.
That turns the primitive into:
This path is more environment-dependent than the minimal proof, so it should not be conflated with the core validated repro.
But defensively, it shows exactly why upgrading untrusted metadata into allowCommands: true is dangerous.
The markdown renderer itself already had the right security idea:
javascript:data:command: for untrusted markdownThat means the global markdown subsystem was not fundamentally broken.
The caller was.
The MCP hover path told the renderer:
this markdown is trusted
when the content source was attacker-controlled server metadata.
That is why this issue is best understood as a trust classification bug.
The renderer only did what the caller authorized it to do.
By the time public stable reached 1.122.0, the vulnerable description path had been hardened.
The fixed code now looks like this:
That is the correct remediation direction.
It fixes the root cause in two ways:
That means:
command: links in descriptions no longer survive as active command linksThis is exactly what a good fix should look like:
It is also worth noting what did not need to change.
The renderer's global trust rules were already reasonable. The fix belonged at the caller boundary where untrusted MCP metadata was being upgraded incorrectly.
Microsoft assessed the issue as:
That is defensible.
The issue required user interaction. It was not a no-click network exploit.
But it was still a real security bug because:
The case classification also matched the bug shape well:
That fits.
This was both:
The real lesson is not "markdown can be risky."
Everybody already knows markdown can be risky.
The real lesson is:
untrusted metadata must not be upgraded into a product-trusted capability surface
That matters even more in modern developer tools because the UI is no longer passive.
It often sits next to:
Once untrusted text reaches a trusted action surface, the bug is no longer cosmetic.
This case also reinforces a second lesson:
when a framework already exposes a safety distinction like
isTrusted, security review should focus hard on every caller that sets it
The vulnerable code was not an exotic parser bug. It was a wrong answer to a simple trust question.
Those are often the best bugs to find because the root cause is both clear and defensible.
MarkdownString(... { isTrusted: true }) and appended the description with appendMarkdown()command: links, so the bug was the caller-side trust decisionopenerService.open(... allowCommands: true)This bug did not need memory corruption, sandbox escape, or complicated race conditions.
It only needed one wrong trust decision:
treat attacker-controlled MCP metadata as trusted markdown
From there, command: execution followed naturally.
That is exactly why this was worth reporting, and exactly why the fix needed to happen at the trust boundary.