Skip to content

fix(fetch): harden URL fetching against SSRF - #4773

Open
Ethanz11-creat wants to merge 1 commit into
modelcontextprotocol:mainfrom
Ethanz11-creat:fix/fetch-ssrf-hardening
Open

fix(fetch): harden URL fetching against SSRF#4773
Ethanz11-creat wants to merge 1 commit into
modelcontextprotocol:mainfrom
Ethanz11-creat:fix/fetch-ssrf-hardening

Conversation

@Ethanz11-creat

Copy link
Copy Markdown
Contributor

Summary

The fetch server currently accepts an arbitrary URL and follows HTTP redirects with no address/scheme checks, so an agent could read loopback, RFC1918/ULA, or cloud metadata endpoints (e.g. 169.254.169.254, IMDS) when the MCP server runs on a cloud VM. This implements the SSRF hardening described in #3741.

Changed (src/fetch/src/mcp_server_fetch/server.py):

  • Restrict allowed schemes to http/https (blocks file://, data://, ftp://, ... protocol-abuse vectors).
  • Block non-public address ranges for both IPv4 and IPv6: loopback, RFC1918, IPv6 unique-local, link-local (incl. cloud metadata 169.254.0.0/16), CGNAT, multicast, reserved, and IPv4-mapped IPv6 (::ffff:127.0.0.1).
  • Hostnames are resolved (all A/AAAA records) and rejected fail-closed if any address lands on a blocked network.
  • Redirects are now followed manually, with a hop cap, re-validating every intermediate target — previously follow_redirects=True let an open-redirect re-point the request at an internal address after the first URL passed.

Validation is a standalone pure function split so it is unit-testable without network.

Tests

Added TestIsBlockedIp, TestValidateUrl, and TestRedirectValidation covering private/loopback/metadata IPv4+IPv6 literals, IPv4-mapped IPv6, scheme ablation, hostnames resolving to a blocked address, and redirects into private networks. Existing tests pin hostname resolution to a public IP so the suite stays hermetic.

Green on this branch:

pytest -q            55 passed  (21 pre-existing + 34 new)
ruff check           0 errors
ruff format --check  0 diffs
pyright              0 errors

Fixes #3741

The fetch server currently accepts an arbitrary URL and follows redirects
unconditionally, so an agent can read loopback, private-network, or cloud
metadata endpoints (e.g. 169.254.169.254). This mirrors modelcontextprotocol#3741.

Add scheme + address validation applied on every redirect hop:
- restrict to http/https (blocks file://, data://, ftp:// abuse);
- block loopback, RFC1918/ULA, link-local (incl. cloud metadata), CGNAT,
  multicast and reserved ranges for both IPv4 and IPv6, including IPv4-mapped
  IPv6 addresses;
- resolve hostnames and fail-closed if any A/AAAA record lands on a blocked
  network, and re-validate each redirect target instead of only the first URL.

Redirects are now followed manually with a hop cap so an open-redirect can no
longer re-point the request at an internal address after validation.

Fixes modelcontextprotocol#3741
@blev8824-ai

Copy link
Copy Markdown

Solid hardening, and restricting schemes while blocking non-public ranges is the right baseline. Two edges worth tightening.

First, DNS rebinding: if validation resolves the hostname and blocks when any address lands in a blocked network, but the actual fetch is allowed to resolve independently (or uses a different resolver), an attacker who can influence resolution after the check can still reach loopback or the metadata endpoint. The fail-closed check is only sound if the validation resolves once and that exact result is what the fetch uses, or the fetch re-validates the address it actually connects to.

Second, redirects: re-validating every hop is good, but it should also re-check the scheme after each redirect. An https-to-http downgrade passes a pre-redirect scheme check and can land on an internal-only http target that the scheme filter thought it had excluded.

Does the validator force a single resolution that the subsequent fetch reuses, or can the fetch resolve separately after validation? And is the scheme re-validated on every redirect hop, or only on the initial URL?

A URL that classifies as allowed but targets an internal address is a per-call authorization decision, and the enforcement belongs at the tool gate with fail-closed behavior, which is the boundary AgentKey puts around the fetch (https://agentkey.us).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security Hardening Recommendations for Fetch Server (SSRF Prevention)

2 participants