| Description | # SSRF in mcp-server-fetch: the fetch tool performs no internal/loopback/metadata IP filtering
### Summary
The fetch tool retrieves a caller-supplied URL with no validation of the destination host. It does not block loopback, link-local/cloud-metadata (x.x.x.x), x.x.x.x, or RFC1918 addresses, follows redirects automatically without re-validating the target, and the robots.txt pre-check uses the same unguarded client. Because the tool's URL argument is produced by the LLM and can be steered by untrusted content (prompt injection from a fetched page or document), an attacker can make the server request internal services and cloud instance metadata and return their contents into the model context. Reproduced on 2026.6.4: a fetch of an internal sentinel URL returned the internal body.
### Details
mcp_server_fetch/server.py, fetch_url (around line 119):
```python
async with AsyncClient(proxy=proxy_url) as client:
response = await client.get(
url,
follow_redirects=True,
headers={"User-Agent": user_agent},
timeout=30,
)
```
There is no host/IP validation anywhere in the module. The only check on the URL is pydantic AnyUrl (a syntactic check). The autonomous-fetch gate, check_may_autonomously_fetch_url (around line 75), fetches robots.txt with the same unguarded client and also follows redirects, so even the pre-flight reaches an attacker-chosen host. follow_redirects=True means a public-looking URL that 302-redirects to an internal target is followed without re-validation. (file:// and gopher:// are rejected by httpx, so this is HTTP/HTTPS SSRF, not local file read.)
### PoC
Reproduced on mcp-server-fetch 2026.6.4 by calling the real fetch_url against a local server standing in for an internal/metadata endpoint:
```python
from mcp_server_fetch.server import fetch_url
# local sentinel at 127.0.0.1:<port> returns "ORCH_REVAL_INTERNAL_IMDS_z1x2c3 iam-creds=admin"
content, prefix = await fetch_url("http://127.0.0.1:<port>/latest/meta-data/iam/", "test-agent", force_raw=True)
```
Actual output:
```
target: http://127.0.0.1:39523/latest/meta-data/iam/ (internal/metadata)
returned content -> 'ORCH_REVAL_INTERNAL_IMDS_z1x2c3 iam-creds=admin'
SSRF: CONFIRMED (fetch_url reached internal sentinel, body returned to LLM)
```
A redirect from an external-looking URL to an internal path is likewise auto-followed and returned. In a real deployment the attacker uses a public URL whose content (read by the agent) instructs the model to fetch http://x.x.x.x/latest/meta-data/... or an internal service, and the response is returned into the model context.
### Impact
An MCP host running the fetch server can be driven to issue requests to internal-only HTTP services and the cloud metadata endpoint, returning their contents (including IMDS IAM credentials and internal service data) into the LLM context, from where they can be exfiltrated by subsequent model output or tool calls. The tool URL is attacker-influenceable through prompt injection (a malicious page or document the agent fetches), so this is reachable without any direct authenticated access. This is a classic SSRF in a widely deployed reference MCP server.
### Remediation
Validate the destination before fetching: resolve the host and reject loopback, link-local (x.x.x.x/16, including the metadata IP), x.x.x.x/8, ULA/RFC1918 and other private/reserved ranges; apply the same check to the robots.txt fetch and re-validate on every redirect hop (resolve and re-check the post-redirect host, or disable automatic redirects and validate each hop). Consider pinning the connection to the validated IP to avoid DNS rebinding, and restricting allowed schemes to http/https. Optionally expose an allowlist/denylist configuration so operators can constrain reachable hosts.
# SSRF in the everything server's gzip-file-as-resource tool (empty domain allowlist by default)
### Summary
The everything reference server's gzip-file-as-resource tool fetches a caller-supplied http/https URL and returns the gzipped response as a resource. Its only host restriction is an allowlist (GZIP_ALLOWED_DOMAINS) that is empty by default, which the code treats as "all domains allowed", and there is no filtering of loopback, link-local/metadata, or private addresses. An attacker who can call the tool can make the server fetch internal services and cloud metadata and read the (gunzipped) response. Confirmed on 2026.1.26: a fetch of a local sentinel returned its body.
### Details
tools/gzip-file-as-resource.js: validateDataURI (around line 96) allows http/https/data and enforces only a domain allowlist; GZIP_ALLOWED_DOMAINS is empty by default and is documented as "Empty means all domains are allowed" (around line 8). fetchSafely (around line 131) then does fetch(url) with default redirect following. There is no loopback / RFC1918 / link-local / metadata IP check.
### PoC
Re-validated on @modelcontextprotocol/server-everything 2026.1.26 by driving the real server over MCP stdio:
```
[sentinel] got request from MCP server: GET /latest/meta-data/ host=127.0.0.1:38339
=== TOOL RESULT ===
DECODED FETCHED BODY: INTERNAL-SSRF-SENTINEL-SECRET-METADATA-token=AKIA_FAKE_4242
MATCH SENTINEL: true
```
The tool call:
```json
{ "name": "gzip-file-as-resource",
"arguments": { "name": "out.gz", "data": "http://127.0.0.1:<port>/latest/meta-data/", "outputType": "resource" } }
```
The server fetched the internal sentinel and returned its body (gunzipped) to the caller.
### Impact
A host running the everything server (a widely used reference/demo MCP server) can be used as an SSRF proxy to internal-only HTTP services and the cloud metadata endpoint, returning their contents to the caller, with the default (empty allowlist) configuration. The URL is attacker-controllable through the tool arguments (LLM-produced, prompt-injection steerable). While "everything" is a demo server, it is frequently run as-is.
### Remediation
Treat an empty GZIP_ALLOWED_DOMAINS as deny-all (or require explicit configuration), and add SSRF protection in fetchSafely: resolve the host and reject loopback, link-local/metadata (x.x.x.x/16), x.x.x.x/8, and private ranges, re-validate every redirect hop, and pin the connection to the validated IP.
# Path traversal in get_file_contents path turns a scoped file read into an arbitrary GitHub-API request under the server PAT
### Summary
The owner, repo, path, and branch parameters of get_file_contents (and create_or_update_file) are string-concatenated into the GitHub API URL with no validation, even though validation helpers exist in the codebase but are not applied here. A path value containing ../ segments escapes the intended /repos/{owner}/{repo}/contents/ prefix, so an attacker can turn a read scoped to one repo into an arbitrary GET (or, via create_or_update_file, PUT) against api.github.com under the server's configured personal access token. Confirmed on 2025.4.8: with owner=octocat, repo=Hello-World, a crafted path returned a file from a different repository (torvalds/linux).
### Details
dist/operations/files.js (around line 54): the request URL is `https://api.github.com/repos/${owner}/${repo}/contents/${path}`. owner/repo/path/branch are only z.string() parsed at dist/index.js (around lines 225-226); the validateOwnerName / validateRepositoryName / validateBranchName helpers are not applied on this path. The token is attached unconditionally (dist/common/utils.js around lines 28-29). An unencoded ../ in path normalizes out of the /repos/{owner}/{repo}/contents/ prefix and redirects the request to any api.github.com endpoint. The scheme+authority is fixed (a userinfo/host override lands in the path, not the authority), so this is request-path forgery within api.github.com under the PAT, not external SSRF or token exfiltration to an outside host; it is bounded by the PAT's permissions.
### PoC
validated on @modelcontextprotocol/server-github 2025.4.8 over MCP stdio (unauthenticated, public repos):
```
get_file_contents({ owner: "octocat", repo: "Hello-World",
path: "../../../../repos/torvalds/linux/contents/Kbuild" })
-> { "name": "Kbuild", "path": "Kbuild", "sha": "a6a0192...", "size": 3129, ... } (torvalds/linux/Kbuild)
```
The arguments name octocat/Hello-World but the returned file is from torvalds/linux, because the ../ segments redirect the request to /repos/torvalds/linux/contents/Kbuild. path=../../../../users/torvalds likewise issues a request to /users/torvalds. With a configured PAT, this reaches the token owner's private repositories and other endpoints, and create_or_update_file (same build) issues PUTs the same way.
### Impact
An attacker who can influence the path argument (LLM-produced, prompt-injection steerable) can read (and, via create_or_update_file, write) GitHub resources outside the intended repository, under the server's PAT and its permissions, including enumerating and reading the token owner's private re |
|---|