Submit #960099: https://github.com/modelcontextprotocol/ servers 2026.6.4 Server-Side Request Forgeryinfo

Titlehttps://github.com/modelcontextprotocol/ servers 2026.6.4 Server-Side Request Forgery
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
Source⚠️ https://github.com/modelcontextprotocol/servers/issues/4492
User
 geochen (UID 78995)
Submission09/04/2026 17:25 (27 days ago)
Moderation10/01/2026 20:15 (27 days later)
StatusAccepted
VulDB entry412764 [modelcontextprotocol mcp-server-fetch/mcp-server-everything up to 2026.6.4 Fetch Tool server.py fetch_url url/path server-side request forgery]
Points20

Do you need the next level of professionalism?

Upgrade your account now!