fix(fetch): harden URL fetching against SSRF - #4773
Conversation
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
|
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). |
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):http/https(blocksfile://,data://,ftp://, ... protocol-abuse vectors).169.254.0.0/16), CGNAT, multicast, reserved, and IPv4-mapped IPv6 (::ffff:127.0.0.1).follow_redirects=Truelet 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, andTestRedirectValidationcovering 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:
Fixes #3741