{
  "question": "Our MCP server returns content other people wrote, such as dataset descriptions, ticket bodies, comments or web pages. Is it our job as the server author to clean that text before it reaches the agent, or is prompt injection through tool results only the client's problem, and what does cleaning it actually involve?",
  "slug": "should-an-mcp-server-neutralize-third-party-text-in-its-tool-results-to-stop-prompt-injection",
  "url": "https://www.rightshold.com/answers/should-an-mcp-server-neutralize-third-party-text-in-its-tool-results-to-stop-prompt-injection",
  "short_answer": "Yes, as one layer, but it won't stop prompt injection by itself. When a tool returns text someone else wrote, that text reaches the model as tool output, and instructions hidden in it can steer the agent. The CKAN MCP server advisory GHSA-c499-9f77-93m8 (moderate, CVSS 3.1 6.9, published July 9, 2026, fixed in @aborruso/ckan-mcp-server 0.4.109) shows how: ckan_package_show copied portal-controlled titles, notes, resource names, descriptions and URLs verbatim into both its markdown and JSON output, so whoever published a dataset controlled the tool result. They could plant instructions telling the agent to call the server's SQL tool and keep quiet about it, and inject fake \"verified\" sections and phishing links into what the user saw. The advisory's remedy is one neutralization pass where results are assembled: defang markdown control characters and directive-looking lines, validate URL schemes before rendering links, wrap the free text in a clearly delimited, non-authoritative block, and tell integrators the content is attacker-influenced. The MCP specification puts duties on both sides: servers MUST sanitize tool outputs, and clients SHOULD validate tool results before passing them to the model. OWASP's MCP tool poisoning page calls fully detecting injected instructions in free text an open problem, so the client's permissions and confirmations still decide how much harm leftover text can do.",
  "checklist": [
    "List every field your tools return that a third party can write: titles, descriptions, notes, comments, file names, URLs, log lines, email bodies. Treat all of them as untrusted, in markdown output and JSON output alike.",
    "Encode them in one place, where tool results are assembled, so a new tool can't skip the step: escape markdown structure (headings, links, images, HTML) and neutralize lines that address the assistant or claim to be system instructions.",
    "Validate URL schemes, for example https only, before rendering a value as a link. Render a URL you can't validate as plain text; the CKAN report showed a url value breaking out of markdown link syntax.",
    "Put third-party text inside a clearly labelled block that says it comes from an outside source, and keep your own status lines outside it, so injected text can't pass as your server's output.",
    "Prefer structured output with a fixed schema (outputSchema and structuredContent) and return only the fields the task needs. Long free-text bodies are where instructions hide.",
    "Don't pair an untrusted-content reader with powerful tools on the same server without a guard. The CKAN advisory rated hijacking the highest-impact harm because the server also exposed SQL and search tools the injected text could invoke.",
    "Say in your README which tools return third-party content, so integrators know which results must never be treated as instructions.",
    "Add a test with a hostile fixture, such as a description containing an instruction, a fake heading and a markdown link, and assert the rendered result keeps it escaped inside the data block."
  ],
  "sections": [
    {
      "heading": "What the CKAN MCP server advisory showed",
      "points": [
        "The CKAN MCP server lets an agent search and read open-data portals. Its advisory says ckan_package_show and its sibling renderers built results by inserting a dataset's title and notes, each resource's name, description and url, and group titles straight into markdown, and the JSON path copied the same fields unchanged. Those fields are free text written by whoever published the dataset, and the server_url argument can point at any portal, including one an attacker runs.",
        "The advisory lists two harms from the one defect. Agent hijacking: a dataset's notes can carry instructions, such as calling ckan_datastore_search_sql, sending out rows that contain \"token\" and not telling the user, and they arrive at the model as tool output. Content spoofing: injected headings, a fake \"Verified by portal admin\" status and phishing links render in the user's client as if the tool produced them. No authentication is needed: publish a dataset on a portal the victim queries, or get the victim to point the tool at your own portal.",
        "It is rated moderate (CVSS 3.1 6.9) under CWE-74, improper neutralization of special elements in output used by a downstream component, and fixed in 0.4.109. The advisory explains the changed scope in its score: the harm lands on the agent and the tools it can reach, not on the CKAN server itself."
      ]
    },
    {
      "heading": "What a server can do at the boundary",
      "points": [
        "The advisory's remedy is a single neutralization pass at the one point where tool results are assembled, covering both markdown and JSON. Every portal-controlled value goes through one encoder that defangs markdown control characters and directive-looking lines and checks URL schemes before a value becomes a link. The encoded text is emitted inside a clearly delimited, non-authoritative block, and the docs tell integrators that portal content is attacker-influenced.",
        "That matches the MCP specification's tools page, which lists sanitizing tool outputs among the things servers MUST do, alongside validating inputs, implementing access controls and rate limiting tool invocations. A server that renders third-party text verbatim lets the attacker choose the bytes the model reads; encoding and delimiting take back control of structure, links and formatting.",
        "What encoding can't do is make a model ignore an instruction written in plain prose inside a field. Escaping removes structure, not meaning, and OWASP's tool poisoning page says fully detecting injected instructions in free-text responses is an open problem. Treat the delimiter as a label that helps the model and the user, not a boundary that holds on its own."
      ]
    },
    {
      "heading": "What the client still has to do",
      "points": [
        "The same specification says clients SHOULD validate tool results before passing them to the model, prompt for confirmation on sensitive operations, and show tool inputs to the user before calling the server. OWASP's prevention list adds: constrain response formats to a fixed schema where possible, isolate privileged tools in a context external servers can't reach, enforce restrictions in the tool execution layer rather than the system prompt, keep an allowlist of approved MCP servers, and require explicit user confirmation for destructive or data-exfiltrating actions.",
        "In practice, an agent that reads public datasets, tickets or web pages shouldn't hold write, send or query tools in the same session without a confirmation step outside the model. The server's neutralization makes injected text harder to pass off as authoritative; the client's permissions decide how much damage text that still gets through can do."
      ]
    }
  ],
  "checks": [],
  "ask_rightshold": {
    "start": "https://www.rightshold.com/start?ref=seo",
    "mcp_endpoint": "https://www.rightshold.com/mcp",
    "ask": "Before my MCP server returns this dataset description to the agent, scan it for hidden text or instructions aimed at the agent.",
    "mcp_call": {
      "tool": "scan_text",
      "arguments": {
        "text": "<the third-party field values your tool is about to return>"
      }
    },
    "also": [
      "Free, no key, up to 100,000 characters per call; scan a large result in parts. It reports exact positions of zero-width and bidirectional characters, hidden markup such as HTML comments, display:none and zero-size text, and base64 blobs (decoded one level), and labels instruction-like language as a heuristic, never a certainty.",
      "It doesn't encode, rewrite or block the text, and an instruction written in ordinary visible prose may not be flagged, so keep your own encoding pass and the client's confirmation step."
    ],
    "scope": "Rightshold isn't affiliated with the CKAN MCP server, the CKAN project, OWASP or the Model Context Protocol project, and it doesn't sanitize tool output or review your server's code. Advisory details are as published in the ondata/ckan-mcp-server repository on GitHub (July 9, 2026), MCP requirements are from the specification's tools page, and attack and prevention notes are from OWASP's community page on MCP tool poisoning, all read November 13, 2026; advisories and specifications get updated, so confirm details on the linked pages."
  },
  "sources": [
    {
      "title": "ondata/ckan-mcp-server: GHSA-c499-9f77-93m8, unsanitized portal-controlled fields are emitted verbatim in tool output (July 9, 2026)",
      "url": "https://github.com/ondata/ckan-mcp-server/security/advisories/GHSA-c499-9f77-93m8"
    },
    {
      "title": "Model Context Protocol specification: Tools (security considerations)",
      "url": "https://modelcontextprotocol.io/specification/latest/server/tools"
    },
    {
      "title": "OWASP: MCP Tool Poisoning",
      "url": "https://community.owasp.org/attacks/MCP_Tool_Poisoning"
    },
    {
      "title": "MITRE CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component",
      "url": "https://cwe.mitre.org/data/definitions/74.html"
    }
  ],
  "related": [
    {
      "title": "Treating Slack, ticket and log content from MCP as untrusted, and adding write scopes one at a time",
      "url": "https://www.rightshold.com/answers/how-to-treat-mcp-tool-output-from-slack-tickets-and-logs-as-untrusted"
    },
    {
      "title": "Stopping prompt injection in fetched web and file content from triggering MCP write tools",
      "url": "https://www.rightshold.com/answers/how-to-stop-prompt-injection-in-fetched-web-or-file-content-from-triggering-mcp-write-tools"
    },
    {
      "title": "Checking your MCP tool descriptions for hidden text before publishing",
      "url": "https://www.rightshold.com/answers/how-to-check-your-mcp-tool-descriptions-for-hidden-text-before-publishing"
    },
    {
      "title": "How to cap MCP tool output size to prevent context flooding",
      "url": "https://www.rightshold.com/answers/how-to-cap-mcp-tool-output-size-to-prevent-context-flooding"
    },
    {
      "title": "Require human confirmation before MCP write tools run",
      "url": "https://www.rightshold.com/answers/how-to-require-human-confirmation-before-mcp-write-tools-run"
    }
  ],
  "computed_from": "Curated guidance; license verdicts computed from Rightshold's license profiles at render time. Not legal advice.",
  "updated": "2026-11-13"
}