fix(fetch): block SSRF to internal/metadata IPs by default#4497
Open
olaservo wants to merge 2 commits into
Open
fix(fetch): block SSRF to internal/metadata IPs by default#4497olaservo wants to merge 2 commits into
olaservo wants to merge 2 commits into
Conversation
The fetch tool issued server-side requests to any URL with no validation of the destination host, and followed redirects without re-checking the target. A prompt-injection-steered URL (or a public URL that 302-redirects to an internal address) could reach loopback, private (RFC1918), link-local, and cloud-metadata endpoints (e.g. 169.254.169.254) and return their contents into the model context. Resolve the host and reject non-public IP addresses (loopback, private, link-local/metadata, multicast, reserved, unspecified), restrict schemes to http/https, and follow redirects manually so every hop is re-validated. The same guard is applied to the robots.txt pre-check. Blocking is on by default; operators who need to fetch internal hosts can opt out with --allow-internal-ips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gets Explicitly block 100.64.0.0/10 (not flagged by is_private before Python 3.13) and unwrap deprecated IPv4-compatible IPv6 addresses (::a.b.c.d, ::/96) so forms like [::127.0.0.1] are classified by their embedded IPv4 address. Brings the fetch guard to parity with the everything server's SSRF classifier. Adds tests for both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds SSRF protection to the
fetchserver. Thefetchtool previously issued a server-side request to any caller-supplied URL with no validation of the destination host, and followed redirects without re-checking the target. Because the URL argument is model-produced and can be steered by untrusted content (indirect prompt injection from a fetched page), this could be used to reach loopback, private (RFC1918), link-local, and cloud-metadata endpoints (e.g.169.254.169.254) and return their contents — including IMDS credentials — into the model context.This change resolves the destination host and rejects non-public IP addresses, restricts schemes to
http/https, and re-validates the destination on every redirect hop.Server Details
mcp-server-fetch)Motivation and Context
The
fetchserver accepts an arbitrary URL and performs a server-side GET. There was no allowlist/denylist for scheme, host, or IP range; no blocking of loopback / private / link-local / cloud-metadata addresses; and redirects were followed withfollow_redirects=Truewithout re-validating the target, so a public-looking URL that 302-redirects to an internal address bypassed any initial-host defense. The robots.txt pre-check used the same unguarded client.What this PR does:
_validate_url_is_safe(): resolves the host (handling IP literals and DNS names, including IPv4-mapped IPv6) and rejects loopback, private, link-local (incl. metadata), multicast, reserved, and unspecified addresses. Restricts schemes to http/https._get_following_redirects) so each hop is re-validated. Applied to both the fetch path and the robots.txt pre-check.--allow-internal-ipsflag (threaded throughserve()and the CLI) restores the previous behavior for operators who deliberately need to fetch internal hosts.How Has This Been Tested?
169.254.169.254, RFC1918,0.0.0.0, IPv6 loopback, IPv4-mapped IPv6), non-http schemes, a public IP literal (allowed), end-to-end rejection throughfetch_url, and the--allow-internal-ipsbypass.uv run pytest(30 passed),uv run ruff check ., anduv run pyright(0 errors) all pass locally.Breaking Changes
Yes — by default the server now refuses to fetch loopback/private/link-local/metadata addresses. Deployments that intentionally fetch internal hosts (e.g.
localhost) must add--allow-internal-ips. This is called out in the README (CAUTION note + new "Customization - Internal IPs" section).Types of changes
Checklist
Additional context
Design note: the
fetchserver's README previously documented internal-IP access as an accepted risk. This PR flips the default to secure-by-default (block, with explicit opt-in) rather than opt-in hardening, since the most severe case (reading cloud instance-metadata / IAM credentials) warrants a safe default. DNS re-resolution between validation and connection leaves a narrow DNS-rebinding window; pinning the connection to the validated IP could be layered on later.🤖 Generated with Claude Code