Rightshold™ Sign up

Answers · MCP security

How do I stop prompt injection hidden in fetched web pages or files from triggering MCP write tools?

Last updated

TL;DR: Track provenance, not just content. Every tool result that carries text you did not author — a fetched web page, a downloaded PDF, a README in a cloned repo, an uploaded spreadsheet — marks the session (or the specific values) as tainted. Once untrusted content is in the context, the host or gateway stops letting the model choose side-effecting MCP tools on its own: a write, send, pay, delete or deploy call goes through only if it was already in the plan or intent declared before the untrusted content arrived, its arguments do not come from tainted values, or a named human approves the exact call. This is the guiding principle of the 2025 prompt-injection design-patterns paper (once an agent has ingested untrusted input, it must not be able to trigger consequential actions) and what Google DeepMind's CaMeL enforces with per-value capabilities. Filters and "ignore instructions in the page" prompts are useful signals, but the model cannot be relied on to ignore hidden text, so the deciding check has to sit outside it.

What should I check, step by step?

1. Label every MCP tool by what it returns: trusted (your own config, values the user typed) or untrusted (web fetch, browser, file read, email, search results, any third-party server). Keep this list in the host or gateway, not in the tool description the server controls.

  1. Taint on read: when an untrusted tool returns, mark the session as tainted, and where your runtime allows, mark the specific values (URLs, email addresses, amounts, file paths) that came from that result.
  2. Write the plan before the read: declare the task's intended side effects (which write tools, which targets) while the context is still clean. After taint, allow only side effects that match the pre-declared plan; anything new needs a human.
  3. Block tainted arguments to sinks: a recipient, URL, repo, amount or shell string that originated in fetched content must not flow into a send, pay, push or exec tool without approval — that is how hidden text exfiltrates data or redirects a payment.
  4. Require approval with the exact call shown: tool name, full arguments, and which untrusted sources are in the context. Bind the yes to a hash of the arguments so the model cannot change them after the click.
  5. Split roles where you can: a quarantined model or sub-agent summarizes untrusted pages into structured fields with no tool access; the planner that holds write tools never reads the raw page (dual-LLM / map-reduce patterns).
  6. Keep detectors as signals, not gates: invisible-text and instruction-like scans on fetched content can raise the bar for approval, but a clean scan must never unlock a write on its own.
  7. Record each taint event, blocked call and approval outside the agent so a reviewer can see what content was in context when a write was allowed.

Why can hidden instructions in a fetched web page or file trigger MCP write tools?

A model reads tool output as more context. White-on-white text in a web page, an HTML comment, a zero-width Unicode run in a file or an instruction in alt text can look like an instruction to the model even though no human sees it. OWASP lists this as indirect prompt injection (LLM01) and the resulting unchecked tool use as excessive agency (LLM06).

How do taint and provenance tracking stop untrusted content from choosing an agent's next action?

Session-level taint is the simple version: after the first untrusted read, every side-effecting call needs a match to the pre-declared plan or a human approval. It is coarse but cheap and catches most real attacks.

How do declared intent and human approval stop prompt injection from triggering writes?

Declare the task's side effects while the context is clean: "create one draft PR in repo X" or "send the summary to the requesting user only". A later call that matches — same tool, same target — can proceed; a call to a new recipient, repo or domain cannot.

How do I ask Rightshold about this (free, no key)?

Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Before my research agent fetches any web pages, declare the only side effect it may take in this task (one draft PR to my docs repo), with enforce on, so a write prompted by fetched content has no matching intent.”

One-click setup

{
  "tool": "declare_intent",
  "arguments": {
    "agent_id": "<one of your agents>",
    "purpose": "research",
    "description": "Docs research task: read public web pages and files; only side effect allowed is one draft PR to github.com/example/docs; no email, payments, pushes to other repos or new domains",
    "operations": [
      "record_ai_run",
      "request_human_approval"
    ],
    "ttl_hours": 4,
    "targets": {
      "domains": [
        "github.com"
      ],
      "resources": [
        "repo:example/docs:draft_pr"
      ]
    },
    "constraints": {
      "enforce": true
    }
  }
}

Rightshold records declared intents, approvals and AI runs you send it and answers verification calls. It does not inspect your agent's context or block calls to other MCP servers by itself; enforcement runs in your host or gateway. No defense is complete against prompt injection. Not legal advice.

What are the sources?

Related

Not legal advice. JSON · Markdown · All answers

Get a free API key in one call

No card and no email needed. 100 free credits; the license and policy checks stay free without a key.

curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_...   # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
  -H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'