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

**Short answer:** 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.

## Checklist
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.
2. 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.
3. 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.
4. 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.
5. 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.
6. 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).
7. 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.
8. 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).
- The danger is the combination: untrusted content plus tools that change things or send data out. A read-only research agent that ingests a poisoned page produces a bad summary; the same agent with a send_email or git_push tool can be steered into acting.
- This is different from a malicious MCP server poisoning its own tool descriptions. Here every server can be honest — the attacker only needs to get text into a page or file your agent will fetch.

## 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.
- Value-level provenance is the stronger version: CaMeL (Debenedetti et al., 2025) has a privileged model write the control flow from the trusted user query, lets a quarantined model parse untrusted data, and attaches capabilities to each value so a policy can refuse a tool call whose arguments came from untrusted sources. The authors report solving 77% of AgentDojo tasks with provable security versus 84% undefended — a real but bounded utility cost.
- The design-patterns paper (Beurer-Kellner et al., 2025) describes six patterns — action-selector, plan-then-execute, LLM map-reduce, dual LLM, code-then-execute and context minimization — that trade utility for isolation. Pick the strictest one your task tolerates; plan-then-execute alone still lets injected text alter the arguments of planned calls, so pair it with argument provenance checks.

## 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.
- When a write falls outside the declaration, pause and show the human the exact call plus the untrusted sources in context ("this session read https://example.com/page and report.pdf"). The approval binds to the argument hash and expires quickly.
- Do not reset taint by asking the model whether the content was safe. Clear it only by starting a fresh context that never saw the untrusted content, or by a human decision recorded outside the agent.

## Ask Rightshold (free, no key)
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code (one-click setup: https://www.rightshold.com/start?ref=seo), 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."

```json
{
  "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
    }
  }
}
```
- Needs your account API key. Rightshold returns a canonical intent statement for the agent's key to sign (sign_intent); verify_intent lets your gateway check the intent token the agent presents before a side-effecting call.
- request_human_approval opens an approval bound to the exact action hash for any write outside the declared intent; verify_human_approval is free and re-checks the receipt right before the write runs.
- record_ai_run can seal what was in context (source URLs and file hashes), the taint flag and each allow or deny decision as a signed record outside the agent.
- This is a record and a check you call, not a taint tracker inside your model host. Rightshold does not see your agent's context; the taint marking and the refusal run in your MCP host or gateway.

_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._

## Sources
- [Debenedetti et al.: Defeating Prompt Injections by Design (CaMeL, 2025)](https://arxiv.org/abs/2503.18813)
- [Beurer-Kellner et al.: Design Patterns for Securing LLM Agents against Prompt Injections (2025)](https://arxiv.org/abs/2506.08837)
- [OWASP GenAI: LLM01 Prompt Injection](https://genai.owasp.org/llmrisk/llm01-prompt-injection/)
- [OWASP GenAI: LLM06 Excessive Agency](https://genai.owasp.org/llmrisk/llm062025-excessive-agency/)
- [Simon Willison: The lethal trifecta for AI agents (June 16, 2025)](https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/)
- [Human approvals on Rightshold](https://www.rightshold.com/human-approvals)

## Related
- [How to require human confirmation before MCP write tools run](https://www.rightshold.com/answers/how-to-require-human-confirmation-before-mcp-write-tools-run)
- [How to treat MCP tool output from Slack, tickets and logs as untrusted](https://www.rightshold.com/answers/how-to-treat-mcp-tool-output-from-slack-tickets-and-logs-as-untrusted)
- [How to mitigate Log-To-Leak prompt injection in MCP](https://www.rightshold.com/answers/how-to-mitigate-log-to-leak-prompt-injection-in-mcp)
- [How to separate read-only MCP tools from side-effecting ones and lazy-load by task](https://www.rightshold.com/answers/how-to-separate-read-only-mcp-tools-from-side-effecting-and-lazy-load-by-task)

_Not legal advice. Updated 2026-11-07. JSON: https://www.rightshold.com/answers/how-to-stop-prompt-injection-in-fetched-web-or-file-content-from-triggering-mcp-write-tools.json_