Hidden Pull-Request Text Can Turn Azure DevOps MCP Review Agents into Confused Deputies
Manifold Security has disclosed an indirect prompt-injection path in Microsoft’s official Azure DevOps MCP server that can turn an AI code-review agent into a cross-project confused deputy. An attacker who can write a pull-request description in one project can place instructions inside an HTML comment. Azure DevOps hides that comment in the rendered review interface, but its API returns the text and the MCP server passes the pull-request response into the model’s context.
In Manifold’s proof of concept, a reviewer’s coding agent read the hidden instructions, ran a pipeline in another project, retrieved a confidential wiki page the attacker could not access, and posted the page back to the attacker-controlled pull request. The researchers reproduced the chain with both Copilot CLI and Claude Code. The result is not a permissions bypass in Azure DevOps itself: every API call uses authority the reviewer already possesses. The security failure is that attacker-authored data can redirect how that authority is exercised.
The research was reported to the Microsoft Security Response Center, which acknowledged and triaged it. No CVE or fixed public release was identified in the disclosure. Manifold tested the official local server; the report does not establish exploitation in the wild or independently demonstrate the same chain against Microsoft’s hosted remote MCP service.
The human and the model receive different pull requests
Azure DevOps pull-request descriptions support Markdown and HTML comments. Text between an encoded <!-- and --> is invisible when a person reads the rendered description, but remains present in the API representation. That creates a dangerous review asymmetry: the person deciding whether a pull request looks safe does not see all of the content their agent will process.
The attacker does not need to address the agent directly. They only need to predict that a reviewer—or an automated review workflow—will ask an agent to ingest the pull request. The injected text can then propose a sequence of otherwise valid tool calls. In the demonstrated chain, the pull request was the untrusted input, the reviewer’s cross-project access supplied private data, and the ability to post a comment supplied the exfiltration channel.
This is the same three-part failure pattern behind prior attacks on GitHub agentic workflows: access to secrets, exposure to attacker-controlled content, and a route to send information out are combined inside one decision-making context. Each capability may be legitimate in isolation. Their composition gives an injected instruction a path from untrusted text to privileged side effect.
Microsoft’s spotlighting control missed the pull-request path
The code-level finding is especially useful because the Azure DevOps MCP project already contains a control for this threat class. Microsoft merged a spotlighting helper in March that wraps external content in randomized delimiters and explicitly labels it untrusted. The project’s createExternalContentResponse function says it should be used for content fetched from Azure DevOps APIs, and wiki and pipeline-log paths call it.
The pull-request tool does not. In the public repository’s July 21 main-branch revision, repo_get_pull_request_by_id retrieves the pull request, optionally adds labels and changed files, and returns JSON.stringify(enhancedResponse) directly. The repository-tools module does not route that response through the shared content-safety helper. Manifold’s finding is therefore a coverage gap in a response-by-response defense, not an absence of awareness that external content is hostile.
Spotlighting is still a probabilistic model-facing mitigation, not an authorization boundary. A complete wrapper would make the payload’s provenance clearer and should reduce successful injections, but it cannot prove that a model will never obey delimited content. The stronger conclusion is architectural: privileged agents should remain safe when content labeling fails on one tool path.
Why reviewer identity becomes the escalation mechanism
Pull-request reviewers are commonly more privileged than contributors. They may have access to sibling repositories, release pipelines, private work items, operational wikis, and organization-level metadata. When the MCP server authenticates as that reviewer, the attacker can attempt to borrow the difference between the two identities without ever receiving the credentials.
This makes “the attacker already needs project write access” an incomplete risk description. Contributor access to a low-sensitivity project is not supposed to grant read access to a separate production project. The injected agent collapses those boundaries because it consumes content from one security domain while carrying an identity authorized in another.
Per-call confirmation can interrupt the demonstrated sequence, but only if the prompt shows the destination, data class, and consequence clearly enough for a person to detect the mismatch. A generic request to allow a wiki read or comment is weak protection, especially when automated review systems deliberately remove those pauses. The site’s coverage of MCP tool-description poisoning illustrates the complementary risk: both tool metadata and tool-returned data can become instruction channels unless the orchestration layer constrains what follows.
Defensive engineering guidance
- Scope review identities to the repository under review. Do not give a general developer credential to an autonomous reviewer. Use a task-specific token or service identity that cannot reach unrelated projects, wikis, pipelines, secrets, or work items.
- Remove unnecessary MCP domains. A code-review agent should not be able to run release pipelines or read operational wikis merely because those tools exist on the same server. Expose an allowlist built for the review task.
- Separate read and write phases. Let one constrained component inspect untrusted pull-request content and produce a structured review. Require a separately authorized component—and explicit policy checks—before posting comments, approving changes, or invoking pipelines.
- Render what the model receives. Review interfaces and approval prompts should reveal HTML comments, hidden Markdown, and other content present in API responses. “Human reviewed” is not meaningful when the human and agent see different inputs.
- Apply untrusted-content handling centrally. Content-safety wrappers should be enforced by a common response boundary with tests that fail when any external-content tool returns raw text. Relying on each tool author to remember one helper creates exactly this type of omission.
- Bind approvals to concrete effects. Confirmation should name the Azure DevOps organization, project, resource, action, and outbound destination. Reject cross-project operations that are not part of the user’s original review request.
- Audit sequences, not only individual calls. Alert when a pull-request review is followed by access to another project, a pipeline run, a sensitive wiki read, or a comment containing data retrieved elsewhere.
- Inspect existing descriptions at the API layer. Search active pull requests for HTML comments and other concealed content, then review agent traces for cross-project tool calls. Absence of visible text in the web UI is not evidence that the API payload is clean.
The Azure DevOps MCP finding demonstrates why prompt injection is an authorization problem as much as a model-behavior problem. Delimiters can help a model recognize hostile data, but the durable control is to prevent any one review agent from simultaneously holding broad private access, consuming contributor-controlled text, and writing to a destination the contributor can read. If all three remain available, one missed content wrapper can become a cross-project data path.
Sources: