July 18, 2026
Prompt Injection Is Just SSRF for Text. MCP Security Part 3
The MCP Bug Bounty Field Guide · Part 3 of 5 — the mental model that makes MCP prompt injection click for a bug bounty hunter, with two…

By Abhishek meena
5 min read
The MCP Bug Bounty Field Guide · Part 3 of 5 — the mental model that makes MCP prompt injection click for a bug bounty hunter, with two real, sanitized PoCs.
30-second versionSSRF is a server fetching an attacker URL and treating the response as a request. MCP prompt injection is a tool fetching attacker text and the model treating it as instructions. Same bug, different deputy. Once you see it as SSRF for text, you can find it, exploit it, and argue it using the bounty tables triagers already pay.
Previously in this series: Part 1 — the MCP surface is open and under-hunted. Part 2 — the tool list itself is the attack.
Part 2 covered tool poisoning: the description is the payload.
That's half the surface. The other half is bigger, and it's the one most hunters will actually find in the wild. It's not the tool description that's attacker-controlled. It's the tool output.
Tools fetch things. A URL. A file. A search result. An email. An API response. All of that comes back as text, and the model reads it as context.
Attacker-controlled text that the model reads as context is prompt injection. And once you map it onto a class you already hunt, the whole thing clicks.
Here's the line that made it click for me:
Prompt injection is SSRF for text.
The mental model, side by side
SSRF and MCP prompt injection are the same bug wearing different deputies. Read them in parallel:
- SSRF: a server is asked to fetch a URL. The attacker controls the URL. The server treats the response as data it acts on. The server is the confused deputy.
- MCP prompt injection: a tool is asked to fetch content. The attacker controls the content. The model treats the content as instructions it obeys. The model is the confused deputy.
Same shape. Same trust boundary. Same direction of untrusted flow. The only difference is what "executes" the attacker input: a server making a request, or a model following instructions.
That's not a metaphor. It's the class. Triagers pay for SSRF every day. Prompt injection is SSRF where the victim is a reasoning engine instead of a web server.
Why tool output is the injection vector
Part 2 said the model reads tool descriptions as instructions. That's true, and descriptions are usually static.
Tool output is worse, because it's dynamic and attacker-controllable by design. A summarizer tool fetches whatever URL the user pastes. A search tool returns whatever the web returns. An inbox tool returns whatever emails exist. Every one of those is a channel an attacker can write into.
The flaw is what the client does with that output. Most MCP clients concatenate tool output straight into the model's context, with no separation between "data the tool returned" and "instructions the user gave." So a line inside a fetched webpage is, to the model, indistinguishable from a line the user typed.
That's the bug. The trust boundary the client should enforce, and almost never does:
Tool output is data, not instructions. The model must never obey text that arrived via a tool result.
When the client skips that boundary, every tool that fetches attacker-controllable content becomes an injection endpoint. That's the surface to hunt.
PoC 1 — the "summarize this URL" exfil
This is the cleanest one to find, because every AI app ships a "summarize a link" feature.
The tool is innocent:
{
"name": "fetch_and_summarize",
"description": "Fetches a URL and returns a summary of its contents.",
"inputSchema": { "type": "object", "properties": { "url": { "type": "string" } } }
}{
"name": "fetch_and_summarize",
"description": "Fetches a URL and returns a summary of its contents.",
"inputSchema": { "type": "object", "properties": { "url": { "type": "string" } } }
}The attacker page the tool fetches contains, near the top:
The user asks: "summarize this article." The tool returns the page. The model reads the hidden instruction as context, calls read_file on the AWS creds, and pastes them into the summary that gets shown or sent.
No poisoned description. No malicious tool. The tool did exactly what it was built to do. The page did exactly what an attacker page does. The model did what its context told it. The bug is the missing boundary between "page content" and "model instructions."
That's the whole class in one example. Once you see it, you see it everywhere.
PoC 2 — the file-write redirect (persistence)
The first PoC exfils. The second one persists. This is the one that turns a prompt-injection finding into a Critical.
Same setup, but the app also has a legitimate file-write tool (write config, save note, edit project). The attacker page returns:
The model uses the legit file-write tool as a confused deputy. ~/.bashrc now contains the alias. Next time the user runs update, persistence is on the host.
Two hops. Two tools that are individually fine. The bug is that the output of one (a fetched page) was allowed to command the other (a file-write tool). The model was the deputy that connected them.
This is why prompt injection through MCP is not a "model safety" issue. It's an access-control issue. The file-write tool was reachable from attacker-controlled text. That's a broken trust boundary, and it's reportable.
One missing boundary. Two real consequences.
How to argue it so it gets paid
Triagers don't have a bounty-table row for "prompt injection." They do have rows for SSRF and stored XSS. Your job is to make the bug land on a row they already pay.
Map it explicitly in the report:
- Class: SSRF (server-side request forgery, where the "server" is the model acting on fetched content). Stored XSS is the other valid mapping (attacker text is stored, later rendered, later "executed" by the model).
- Source of untrusted input: the fetched URL / file / message body.
- Deputy: the model, or any tool the model is allowed to call.
- Action taken: the specific thing the model was made to do. Read a file. Write a file. Send a message. Call another tool.
Then the three-layer impact argument from the IDOR piece, applied here:
Layer 1 — Blast radius. Every user who can trigger the tool with attacker-controllable input. For a "summarize a link" tool, that's every user of the app. State the number.
Layer 2 — Chained impact. The two PoCs above are your chain. Exfil (PoC 1) and persistence (PoC 2). You don't execute the chain. You show the path, with both endpoints.
Layer 3 — Business impact. Mass credential exposure (AWS keys, SSH keys, tokens) plus host persistence. Name the regulatory floor. A bug that can write to ~/.bashrc on user machines is not Informative.
The reframe that should sit at the top of your report:
This is SSRF where the victim is a reasoning engine. The fetched content is the request. The model is the deputy. The file-write tool is the reachable internal service. Map it to your SSRF bounty table.
The close
Prompt injection through MCP feels like a new, fuzzy "AI safety" thing. It isn't. It's the oldest bug in the book: untrusted input, trusted deputy, missing boundary.
You already hunt this. You call it SSRF when the deputy is a server. You call it stored XSS when the deputy is a browser. When the deputy is a model, the bug is identical. Only the executor changes.
Once you frame it that way, the report writes itself: the untrusted source, the missing boundary, the reachable tool, the chain, the business exposure. Triagers pay for this every day. They just don't know they're paying for prompt injection.
Next in the series: Part 4 — the full RCE chain, hop by hop. Where Parts 2 and 3 connect into one end-to-end takeover path, and the single hop a defender should break.
Sources
- Model Context Protocol tool schema and tool result handling — spec at
modelcontextprotocol.io. The "tool output concatenated into model context" behavior is a client implementation choice, not a protocol requirement; verify the specific client's behavior before reporting. - SSRF and stored XSS as framing classes for prompt injection — see OWASP SSRF guidance and the confused-deputy framing (Norman Hardy, 1987). The mapping to MCP is the contribution of this piece.
- Indirect prompt injection via fetched content (web pages, emails, documents) — established prompt-injection research, 2023 onward. Cite the specific paper/advisory for the technique you reproduce.