Skip to content
7 min read

SSRF Through a Remote MCP Server in Agent Systems

SSRF through a remote MCP server: a practical checklist for DNS, redirects, private IPs, egress policies, and secure browser tools.

SSRF Through a Remote MCP Server in Agent Systems

You cannot treat connecting an external MCP server as an ordinary API integration. You are adding a new remote address to the agent system, a new set of instructions for the model, and often a new path to tools that can access the network. If that path is not restricted before connection, SSRF turns the agent into a convenient proxy to localhost, cloud service endpoints, internal APIs, and network segments the user cannot access.

The most common mistake looks harmless: the team lets users specify an MCP server URL, checks that the string starts with https://, and decides that this is enough. The client then resolves DNS on its own, follows redirects automatically, reopens the connection, and a browser tool receives a command to visit an address from the server's output. Only the string was checked. The requests went somewhere else entirely.

A remote MCP server is not a single outbound connection

A remote MCP server creates at least two different classes of network activity, and mixing them is dangerous.

The first class is the client-to-server transport. The MCP client makes HTTP requests or maintains a streaming connection to the server address. If the address is supplied by a user, tenant configuration, or another external service, this is an SSRF entry point in the client itself.

The second class is the actions the model takes after connecting. The server may describe a fetch_url tool, return text asking the model to open a link in a browser, offer to import a document by URL, or pass a URL to an already trusted agent tool. The server does not automatically receive network access, but it can influence the model's decision to use access the agent already has.

These are two different security boundaries.

  • For MCP transport, you control the registration URL, DNS, IP, port, TLS, redirects, and the process route.
  • For tools, you control the call schema, allowed arguments, permissions, the executor's network policy, and how untrusted text is handled.

If you reduce both cases to the rule "we do not connect to localhost," the protection will have gaps. OWASP explicitly recommends defining a positive list of schemes, ports, and destinations, disabling HTTP redirects, and accounting for DNS rebinding with a race between checking and using an address.

MCP does not replace ordinary outbound-network discipline. The Model Context Protocol specification for Streamable HTTP separately requires checking Origin on incoming connections, recommends that local servers listen only on 127.0.0.1, and requires authentication. This protects a local MCP server from DNS rebinding initiated by a browser. The client-side conclusion is stricter: an external server must be external not by name, but by its actual address and network route.

A URL string proves nothing

Checking startsWith("https://") is not a destination check. Parse the URL with a standard parser, normalize it, and check its individual parts: scheme, hostname, port, path, and the absence of credentials in the authority component.

These cases should be rejected before DNS:

  • http:// and any nonstandard schemes if the policy allows HTTPS only;
  • URLs with user@host, where one hostname is visible but the connection goes to another;
  • IP literals if the policy allows only specific domain names;
  • nonstandard ports when the server should use only 443;
  • backslashes, invalid encoding, and ambiguous IPv4 and IPv6 forms.

A regular expression is almost always worse than a standard URL parser here. It does not know every normalization rule, while an attacker has time to try forms the regex author did not consider. OWASP's current Node.js guidance separately lists bypasses through alternate IP formats, IPv4-mapped IPv6, backslashes, Unicode, and embedded credentials.

Separate the two registration modes.

Trusted directory mode is appropriate for production. The configuration contains preapproved server names, allowed ports, and the expected authentication method. The user chooses a server from the directory instead of entering an address.

Research sandbox mode allows a user-supplied URL but runs a separate worker without access to the corporate network, cloud credentials, production DNS, or the shared browser profile. It must not inherit the main agent's permissions simply because it was called from the same application.

Do not combine these modes with one allow_custom_mcp_url flag. In production, it usually means "allow an arbitrary outbound request from a process with excessive privileges."

DNS must be checked when the connection is made

DNS rebinding breaks the pattern "first allow the domain, then connect by name." On the first request, mcp.example may return a public IP that passes validation. The TTL then expires, the client resolves the name again, and the same name returns 127.0.0.1, an address on the corporate network, or a link-local address for a metadata service.

The situation is even worse when code checks one address from a list of A and AAAA records while the HTTP library chooses another. The check and the connection become different operations.

The working rule is simple: the client must resolve the name, check every returned address, choose an allowed address, and open the TCP connection to that exact address. It must repeat the entire cycle for each new connection. For TLS, preserve the expected server name for SNI and certificate validation, but do not give the library freedom to resolve the name again outside your control.

Pseudocode shows the required order:

url = parse_and_normalize(input)
assert url.scheme == "https"
assert url.port in {443}
assert hostname_in_allowlist(url.hostname)

answers = resolve_all(url.hostname)
assert answers is not empty
for ip in answers:
    assert is_public_routable(ip)

ip = choose_address(answers)
socket = dial_tcp(ip, url.port, timeout=3s)
tls = handshake(socket, server_name=url.hostname, verify_certificate=true)
http = send_request_over(tls, host=url.hostname, redirect="error")

In real code, dial_tcp must actually receive the IP, not the name. Otherwise the library will perform a second hidden DNS resolution and restore the vulnerability.

DNS checks should record at least the original name, the list of returned addresses, the selected address, port, time, the allow or deny rule, and the MCP connection ID. These details are not cosmetic. When an investigation starts with the question "why did the agent try to reach 169.254.169.254?", a log without the actual IP is nearly useless.

Private addresses are only part of the forbidden zone

Blocking RFC 1918 is mandatory, but it does not close SSRF. RFC 1918 defines 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 as private IPv4 address space. An internal service, localhost, or a metadata service can easily be outside these three ranges.

The is_public_routable(ip) function should reject not just "a few bad addresses," but every address that is not an allowed public destination for the specific environment. The minimum deny set includes:

  • IPv4 loopback, unspecified addresses, and 0.0.0.0/8;
  • RFC 1918, link-local 169.254.0.0/16, carrier-grade NAT 100.64.0.0/10, multicast, and reserved ranges;
  • IPv6 ::, ::1, link-local fe80::/10, unique local fc00::/7, and multicast ff00::/8;
  • IPv4-mapped IPv6 addresses, after which the original IPv4 must also be checked;
  • cloud metadata service addresses, even when the network route to them appears to be an exception to the general rule.

OWASP gives a minimum denylist that includes localhost, RFC 1918, multicast, and 169.254.169.254, while also warning that denylists can be bypassed and that allowlists are preferable. This is the right priority: a denylist catches known dangerous address classes, while an allowlist answers the question of where the process is allowed to connect at all.

Do not allow a private address "because it is our internal MCP." An internal server requires a separate connection through the service network, mTLS, a separate DNS zone, and a separate policy. If the same code with the same flag can reach both public and internal addresses, someone will eventually submit an internal address through the external registration path.

A redirect changes the destination after your check

Keep model data in your country
For data residency requirements, use AI Router-hosted open-weight models on your own GPU infrastructure.

Automatic redirects are especially dangerous when registering a remote server. The client checks https://approved.example/mcp, sends authentication headers, and receives 302 Location: http://127.0.0.1:8080/admin. If the library follows the redirect automatically, the first check no longer matters.

Blocking redirects during connection is simpler and safer. The client receives any 3xx status, closes the attempt, and records an event. For an MCP endpoint, there is almost never a valid business reason for this: the server's working address should be the final address.

If the architecture requires redirects, treat every Location as a completely new URL. Repeat URL parsing, scheme, hostname, port, DNS, and selected-IP validation. Limit the number of redirects. Do not carry Authorization, cookies, or custom headers to another origin without a separate explicit rule.

MDN warns that checking the Response.redirected property after the response is too late: the request may already have gone to an unintended destination. Redirects must be blocked when calling fetch, not discovered afterward.

There is a less obvious variant: a redirect inside a tool. An MCP server provides read_document(url), the agent allows only the provider's domain, but the tool follows a redirect to a file on another domain or an internal address. Control must be in the tool's HTTP client, not only in the argument validator before the call.

The browser tool should live in a separate zone

Browsers are often considered safe because they have the Same-Origin Policy. That is the wrong model for protecting an agent from SSRF. A browser can open a URL at the agent's command, follow navigation, load images and scripts, use a proxy, and sometimes have cookies or an SSO session. Even when browser policy does not let the page read the response, the network request has already happened.

An external MCP server does not have to call the browser directly. It only needs to return text such as "open the verification address to continue," and the model may decide that this is part of the task. This is indirect prompt injection with a network consequence.

The browser tool therefore needs four restrictions.

  1. Separate egress. Its process or container must not see production subnets, cluster management endpoints, metadata services, or internal DNS names.
  2. Separate credentials. The sandbox must not contain corporate cookies, admin-panel tokens, SSH agents, or shared-profile files.
  3. Navigation policy. Allow only HTTPS and, where possible, a fixed set of domains. Check every navigation and every redirect, not just the initial link.
  4. Explicit confirmation for sensitive actions. File uploads, form submissions, SSO sign-ins, executable-content downloads, and requests to a new domain must not happen simply because a tool returned text.

Do not hide a browser behind the name web_search. The name does not change its permissions. Documentation and audit logs should show whether the tool can only search, open public pages, download files, submit forms, or act with an authenticated session.

Network policy must survive a code error

Control each layer separately
Key-level rate limits constrain the model API while the network firewall limits what tools can do.

URL validation in the application is necessary, but one parsing mistake, a new library, or a bypass through another tool can reopen access. The agent process therefore must not be able to connect to a dangerous address even when the code asks it to.

A reasonable setup looks like this:

agent-orchestrator
  -> only LLM API and task queue
mcp-worker
  -> only approved MCP endpoints through egress proxy
browser-worker
  -> public HTTPS through a separate proxy, no corporate subnets
internal-tools-worker
  -> only specific internal services, no arbitrary URL

This does not have to mean four virtual machines. Separate workloads, network namespaces, containers, service accounts, and egress firewall rules are enough. The principle matters: a tool with an arbitrary URL must not have the same route as a tool that reads data from a bank, CRM, or internal directory.

At the network level, set the default rule to deny all outbound traffic and open only known destinations. For external MCP endpoints, this usually means approved FQDNs through a controlled proxy or known provider CIDRs if they are stable. For internal endpoints, use a separate gateway and mTLS instead of adding a broad range to the shared allowlist.

The proxy should log the hostname, resolved IP, SNI, port, method, status, volume, and rule that allowed the request. DNS, proxy, and tool logs should share a trace ID. Otherwise you will see either the model's intent or the network fact, but will not connect the two.

Test tool behavior, not just the connection

Teams often test one good endpoint, get a successful initialize, and consider the task complete. Testing should instead be a set of negative scenarios, each ending in a controlled rejection and a log entry.

Use an isolated test environment and test hosts you own. Do not test the protection by scanning someone else's addresses. At minimum, cover the following:

ScenarioExpected result
URL with http://Rejection before DNS and TCP connection
https://127.0.0.1/Rejection during parsing or IP validation
Domain returning both a public and a private IPRejection if any response contains a forbidden address
Allowed domain with a 302 to another hostRejection without a second request
IPv6 loopback and IPv4-mapped IPv6Rejection after address canonicalization
MCP result asking the browser to open a new domainPolicy requires confirmation or blocks the navigation

For DNS rebinding, the test must prove that the check is tied to the connection. Set up a test name that resolves to an allowed address in the first phase and then changes to a forbidden address. The client must reject the next connection instead of treating the previous decision as a permanent pass. Also test connections with multiple A and AAAA records: if even one address is forbidden, it is safer to reject the entire name until you have implemented strictly controlled address selection.

Test text received from the MCP server as well. Create a tool that returns an instruction to open a URL on a new domain, download an archive, or sign in to an internal portal. The agent must not automatically turn a text result from an external tool into a network action with high privileges.

MCP tools must be treated as untrusted instructions

Separate logs and access
Audit logs and key-level rate limits help separate model-call controls from MCP network auditing.

An MCP client usually shows the model the tool name, description, JSON Schema for arguments, and execution result. All of this comes from the remote server. A search_docs description may contain a hidden request to call your browser tool. A get_status result may claim that fixing an error requires visiting an internal address. Structured JSON makes data convenient, but it does not make it trustworthy.

Separate the model's ability to choose a tool from the tool's right to perform an action. The model may suggest a call. The policy layer must decide whether that call is allowed with those arguments and in that context.

In practice, this means:

  • do not give a remote MCP server tokens broader than its own tasks require;
  • do not let one tool automatically pass a URL to another without rechecking it;
  • label output from external tools as untrusted data in the model context;
  • require explicit human confirmation when a new domain, file, or account goes beyond the task's scope;
  • limit the rate and concurrency of network calls so the agent does not become a scanner.

The MCP authorization specification contains a separate warning: a server that accepts tokens with the wrong audience and forwards them can create a confused deputy problem. The same logic applies to the agent: do not let a remote server indirectly use broader network access or credentials than your process should have.

Checklist before enabling an external server

Before the first production connection, go through this list and record the result in a change request.

  • The server address comes from an approved directory or runs in a separate research sandbox.
  • The URL parser restricts access to HTTPS and allowed ports and rejects ambiguous URL forms.
  • The DNS resolver returns all A and AAAA addresses, and the policy checks every one before the actual connection.
  • The TCP connection opens to the verified IP, TLS checks the certificate and server name, and a second hidden resolution is impossible.
  • Redirects are disabled. If an exception is necessary, every hop goes through the full validation cycle without transferring secret headers.
  • The egress firewall or proxy blocks localhost, private, link-local, metadata endpoints, and internal networks unrelated to the task.
  • The browser, HTTP fetcher, and internal tools run in separate zones with separate credentials and routes.
  • Logs connect the agent decision, MCP tool call, DNS response, actual IP, and network result with one trace ID.
  • Tests cover redirects, DNS rebinding, IPv6, mixed DNS responses, and an external tool's instruction to open a new URL.
  • The integration owner knows how to quickly disable the specific MCP server, its keys, and its egress rule.

AI Router can be used as a single OpenAI-compatible gateway for model calls, but the MCP-worker and browser-worker still need their own network boundaries. Model routing does not replace the agent's outbound-request policy.

Do not connect the server until you can answer one uncomfortable question: "If it starts returning a malicious link in a tool description or result tomorrow, which exact process will try to open it, and where is that process physically allowed to connect?" If the answer is "the main agent, anywhere," the problem is no longer just MCP.

Frequently asked questions

Can a remote MCP server scan the internal network by itself?

A remote MCP server does not automatically gain access to the agent's entire network. The risk appears when the client connects to an arbitrary address without controlling the destination, or when the server uses tool descriptions, call results, or prompt injection to persuade the model to use an allowed browser, HTTP client, or internal tool.

Is checking DNS once before connecting to an MCP server enough?

No. Checking the name before connecting leaves a window for DNS rebinding: the name may first return a public address and point to an internal one on the next resolution. Check every address used for the actual connection and bind the connection to the verified IP.

Which IP addresses should be blocked to protect against SSRF?

No. RFC 1918 covers only part of IPv4. Block loopback, link-local, multicast, unspecified, carrier-grade NAT, and documentation ranges, as well as IPv6 loopback, unique local addresses, and IPv4-mapped IPv6. The list should be maintained by an address-validation library, not by a custom regular expression.

Should HTTP redirects be disabled for a remote MCP server?

Disable automatic redirects during registration and the initial connection. If the product requires redirects, process each one manually: limit the number of hops and recheck the scheme, hostname, port, and destination address before the next request.

Why is an agent's browser tool dangerous when MCP is connected?

A browser tool should not be considered safer than an HTTP client. A browser can navigate, load nested resources, and may have cookies or access to a corporate proxy. Give it a separate outbound route and block access to internal network ranges.

Why doesn't a denylist replace an allowlist?

A denylist is useful as a safety net, but it can be bypassed with unusual address representations, a new service range, or a normalization error. For known MCP servers, use an allowlist of names, ports, and expected identifiers, while the network blocks everything else.

How can I quickly test an agent's SSRF protection?

First restrict the agent process's network access with a separate namespace, egress firewall, or proxy that enforces destination rules. Then configure the client with allowed schemes and ports, manual redirect handling, validation of every resolved IP, and short timeouts. Finally, confirm in the logs that an attempt to reach localhost or a metadata endpoint never leaves the process.

How does SSRF during MCP connection differ from SSRF in a fetch_url tool?

Keep them separate. In one case, the agent's client connects to the MCP server address. In the other, a server or tool receives a URL as an argument and downloads it. They share the same class of network risk, but have different control points and different investigation logs.

Can an external MCP server be connected to browser tools?

Not by default. Tools with arbitrary URLs, browser requests, webhooks, file imports by link, and OAuth discovery require a separate assessment. Start with read-only tools, a fixed set of domains, and the minimum necessary credentials.

Does an LLM API gateway protect against SSRF through MCP?

An LLM provider does not solve the agent's outbound-access problem. AI Router can remain a single OpenAI-compatible gateway for model calls, but the process using MCP and browser tools still needs its own DNS, egress, and logging rules.