Skip to content

+ Intertrace Threat ResearchTLP:CLEAR7 sources

Threat research10 min read

Before the first tool call: SSRF in MCP discovery and local launch as code execution

Before any tool is called, an MCP client fetches URLs the server chose and may start local processes. A field guide to the six decisions that keep the connection handshake from becoming SSRF or code execution, with the tests that prove each one.

How attacks on AI agents work in practice, from public incidents and our own test corpora, and the controls that stop them.

October 3, 2026 · 10 min read

MCPSSRFOAuth DiscoveryLocal ExecutionCVE-2025-6514
Contents
  1. 01Decision 1: list every URL you fetch, and who chose it
  2. 02Decision 2: know where the client runs, because that sets the blast radius
  3. 03Decision 3: use tested address checks, never your own parser
  4. 04Decision 4: let internal servers through by name, not by loosening the ranges
  5. 05Decision 5: open the authorization URL without a shell
  6. 06Decision 6: decide who may start a process, and what it can reach
  7. 07Eight requests to send before you trust the client

A platform team builds an internal agent service with a feature that lets any team connect an MCP server by pasting its URL. Then a penetration tester pastes the URL of a server they control, and the service, running inside the cloud network with an instance role, requests whatever address that server tells it to. The finding lands in the blind spot of a threat model built around tools/call, because no tool was ever called.

Most MCP security reviews start at tools/call. That gate matters, but connection setup happens earlier and the server drives it. Treat the handshake as untrusted input and every process launcher as a remote code execution service until proven otherwise. The decisions below come in the order a team meets them.

Decision 1: list every URL you fetch, and who chose it

The MCP authorization specification builds on OAuth discovery [2]. The client's first request gets 401 Unauthorized. The server's WWW-Authenticate header may carry a resource_metadata URL, which the client must use if present; otherwise it probes well-known paths. That Protected Resource Metadata document lists authorization_servers, and the client fetches each one's metadata from well-known paths to get the token_endpoint and authorization_endpoint [2].

Only the first URL came from your configuration. The resource metadata URL came from the server's header, the authorization server from the server's document, and the endpoints from metadata hosted by an authorization server the server named. The MCP security guidance lists exactly these as fields a malicious server can fill with internal addresses [1], and RFC 9728 warns that because clients talk to servers they have no prior knowledge of, the design opens a risk of SSRF [3].

Requests a client makes before any tool callFig. 1
CLIENT REQUESTS BEFORE THE FIRST TOOL CALL1POST https://srv/mcp → 401FROM CONFIG2GET resource_metadata URLFROM SERVER3GET AS metadata (well-known)FROM SERVER4POST token_endpointFROM AS METADATA5open(authorization_endpoint)FROM AS METADATAWhere it can point169.254.169.254localhost:637910.0.0.0/8 admina token endpointa shell, via open()four of the five URLs come from the server you are connecting to · validate each one, and every redirect hop, before you fetch

Five requests in the MCP connection handshake and who supplies each URL. Only the first comes from your configuration; the rest come from the server or from metadata it pointed to, and each can be aimed at internal addresses or, through a URL opener, a shell.

URLs in the MCP connection path, who controls them, and the check each needs
URLSupplied byWhat an attacker aims it atCheck
resource_metadata in WWW-AuthenticateThe MCP serverCloud metadata, localhost services, internal IPs [1]HTTPS only; block private, loopback and link-local ranges; confirm the document's resource matches the URL used [3]
authorization_servers in resource metadataThe MCP serverInternal identity or admin hostsSame address checks; for server-side clients, an allowlist of authorization servers
token_endpoint and other metadata endpointsThe authorization server the MCP server namedPOST requests that change state on internal services [1]Validate before every request, not only at discovery
authorization_endpointAuthorization server metadataThe browser or, through a careless opener, a shell [1][5]Allow only http and https; open without a shell
Redirect targetsWhoever answers the previous hopAn internal address behind a public first hop [1][4]Disable automatic redirects or validate each hop
client_id URL (Client ID Metadata Documents)The client, fetched by the authorization serverThe authorization server's own network [2]SSRF protection and a domain trust policy on the authorization server

The last row is the mirror image. The 2025-11-25 specification recommends Client ID Metadata Documents, where a client's identifier is an HTTPS URL that the authorization server fetches, and notes that a malicious client can use it to make the authorization server request arbitrary URLs, such as private administration endpoints [2]. If you operate an MCP authorization server, its fetcher needs the same controls as a client's.

Decision 2: know where the client runs, because that sets the blast radius

On a developer laptop, a forged discovery request reaches the laptop's localhost services and home network. On a server-side client, such as an agent platform in a cloud network, it reaches whatever that host can reach: the instance metadata service at 169.254.169.254, a Redis on localhost:6379, an internal admin panel. The guidance lists the consequences: credential exfiltration from metadata endpoints, internal reconnaissance through error messages, state changes through POST requests to token endpoints, firewall bypass with the client as proxy, and data exfiltration when internal responses are reflected back [1].

Reflection is the detail teams miss. A forged request is harmless if the client discards the body. It becomes a leak when error handling puts the response in a log line the user can see, an error returned to the caller, or text in the agent's context, where the model will summarize it.

Here is how that plays out on an illustrative analytics company's agent platform, which lets teams register MCP servers by URL. A contractor's server answers the first request with a 401 whose resource_metadata URL points at the instance metadata path for security credentials. The platform fetches it. The body is not valid JSON, so the parser fails and the error message shown in the registration screen includes the role name; a second registration asks for that role's credentials path, and if tokenless metadata access is allowed, the error now contains temporary cloud credentials. An egress proxy, a range check, token-based metadata access and a generic error would each have broken that chain on its own.

Decision 3: use tested address checks, never your own parser

The guidance is specific and worth following line by line [1]. Its central warning is not to write the address checks yourself, because attackers use octal and hexadecimal notation and IPv4-mapped IPv6 addresses that custom parsers miss.

  • Require HTTPS for every OAuth-related URL in production, allowing plain http only for loopback during development, with an explicit opt-out for test setups [1].
  • Block private and reserved ranges, including 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, loopback, link-local 169.254.0.0/16 and private IPv6, as RFC 9728 recommends [1][3].
  • Resolve the hostname once, check the resolved addresses, then connect only to a checked address while keeping the original hostname for TLS. OWASP notes that a second, unchecked DNS lookup between validation and connection defeats the check, which is how DNS rebinding works [4].
  • Turn off automatic redirects and validate each hop with the same scheme and address rules [1][4].
  • Route discovery traffic through an egress proxy that enforces the destination policy, so application-level checks are not the only barrier [1].
  • Reject resource metadata whose resource value does not match the URL used to fetch it; RFC 9728 says such a response must not be used [3].
  • Keep internal response bodies out of user-facing errors and model context. Return a generic failure with a correlation ID and keep detail in protected operator logs.
  • Attach no credentials to discovery requests, and on cloud hosts require token-based instance metadata access such as IMDSv2, which OWASP describes as defense in depth against some SSRF [4].

Decision 4: let internal servers through by name, not by loosening the ranges

Some deployments legitimately connect to internal MCP servers, and a blanket private-address ban breaks them. The tempting fix is to relax the range checks for any server that asks. Do not. Keep the ranges blocked and maintain an explicit, operator-owned list of internal hosts the client may reach. That is how the analytics company above kept its own internal servers working after the fix.

Decision 5: open the authorization URL without a shell

The last discovery step hands the client a URL to open in a browser. mcp-remote, a widely used proxy that lets clients with only local transport support reach remote MCP servers, passed the server-supplied authorization_endpoint to the npm open package [5]. JFrog showed a crafted value led to command injection: on Windows, arbitrary commands with full parameter control; on macOS and Linux, execution of arbitrary executables with limited parameter control. Versions 0.0.5 through 0.1.15 were affected, 0.1.16 fixed it, and the CVSS score is 9.6 [5].

The specification now spells out the rule [1]. Allow only http and https schemes for authorization URLs and reject javascript:, data:, file: and similar schemes. Never open URLs through shell commands such as cmd.exe, PowerShell or sh; use platform-specific, non-shell mechanisms. Web-based clients should add a content security policy, because a javascript: URL opened in the client's context is cross-site scripting, which the guidance describes escalating to code execution through a local stdio proxy [1].

Decision 6: decide who may start a process, and what it can reach

A local MCP server is a program the client starts with a command, arguments and an environment. The specification names the risks: a malicious startup command in a client configuration, a payload shipped inside the server, or an insecure server left running on localhost and reached through DNS rebinding [1]. Its example chains a package install with a curl that posts the user's SSH key to an outside host. Clients offering one-click local setup must show the exact command, untruncated, mark it as code execution, and require explicit approval [1].

The launcher is an endpoint too. MCP Inspector, the project's debugging tool, ran a local proxy that could start MCP servers over stdio. Before 0.14.1 it had no authentication between the Inspector client and the proxy, and its SSE endpoint accepted a command and arguments, so a crafted request could run an arbitrary program [6]. Oligo Security showed the request could come from a malicious public web page, using browsers' handling of 0.0.0.0 or DNS rebinding to reach a service bound to localhost. The fix added a session token and Host and Origin header checks [6]. Any process that spawns MCP servers on request is a remote code execution service unless it authenticates every caller.

Three inputs that can start a process as youFig. 2
Config entrycommand and argsWeb pagerequests to localhostServer URLcrafted auth URLEXACT COMMANDTOKEN + ORIGINNO SHELL OPENLauncherclient or local proxySANDBOXProcess as yousame rights as youHOME DIR · SSH KEYSCLOUD CREDS · NETWORKanything that can start an MCP server is a code-execution endpoint · authenticate it, show the exact command, sandbox what it starts

A configuration entry, a web page reaching a localhost launcher, and a crafted authorization URL can each make a client or local proxy start a process with the user's privileges. The control on each path is shown on its arrow, and a sandbox limits what the process can reach.

Then contain what starts. The guidance recommends sandboxing MCP servers with minimal default privileges and restricted file system and network access, granting more only explicitly [1]. Anthropic's write-up on sandboxing its coding agent explains why both kinds of isolation are needed: without network isolation a compromised process can send out files like SSH keys, and without file system isolation it can escape the sandbox and gain network access [7]. Pass a server only the environment variables it needs, never the parent's whole environment, and never start an unreviewed server just to list its tools.

Eight requests to send before you trust the client

Run these against a controlled test server in an isolated network, with harmless fixtures in place of real targets. Never test by launching an unknown package on an analyst's workstation.

  • Return a resource_metadata URL of http://169.254.169.254/ and of http://127.0.0.1:6379/ and confirm the client refuses both before connecting.
  • Return a public HTTPS URL that redirects to a private address and confirm the client refuses the second hop.
  • Serve a hostname that resolves to a public address first and a private one second, and confirm the connection uses only the checked address [4].
  • Return resource metadata whose resource value differs from the URL fetched and confirm the client discards it [3].
  • Return an internal-looking HTML body from a forged request and confirm none of it appears in errors or model context.
  • Supply authorization_endpoint values with javascript:, file: and shell metacharacters and confirm the client rejects them and never invokes a shell [1].
  • From a browser page on another origin, send requests to every local launcher or proxy port you run and confirm each is rejected without a valid session token [6].
  • Launch an approved local server and confirm home-directory credentials and arbitrary outbound hosts are out of its reach [7].

These controls belong to the client, its host and your egress network, because discovery, URL opening and launches all happen inside the client. A gateway does not replace them. Intertrace works around them: the first call to an unapproved MCP server asks a person once, launch packages are checked against OSV, Discovery finds MCP configurations committed to GitHub so you know which launch commands exist, and once traffic routes through the gateway every tool call is decided with its real arguments and fails closed when it cannot be decided.

Sources

Reporting, research and specifications this piece draws on. Links go to third-party sources; analysis and recommendations are Intertrace’s own.

  1. 01Security Best Practices (specification version 2025-11-25) · Model Context Protocol
  2. 02Authorization (specification version 2025-11-25) · Model Context Protocol
  3. 03RFC 9728: OAuth 2.0 Protected Resource Metadata · IETF
  4. 04Server Side Request Forgery Prevention Cheat Sheet · OWASP
  5. 05CVE-2025-6514 Threatens LLM clients · JFrog Security Research
  6. 06Critical RCE in Anthropic MCP Inspector (CVE-2025-49596) Enables Browser-Based Exploits · Oligo Security
  7. 07Making Claude Code more secure and autonomous with sandboxing · Anthropic

About this desk

Intertrace Threat Research

Threat research

How attacks on AI agents work in practice, from public incidents and our own test corpora, and the controls that stop them.

All 18 pieces by Intertrace Threat Research

Dated October 3, 2026 · added to intertrace.ai October 17, 2026

+ Runtime enforcement

See these controls on your own agent traffic.

Intertrace sits inline between your agents and the models, tools and MCP servers they call, and decides every call before it runs.

Keep reading

All insights →