Skip to content

fix(everything): block SSRF to internal/metadata IPs in gzip-file-as-resource#4498

Open
olaservo wants to merge 6 commits into
modelcontextprotocol:mainfrom
olaservo:fix/everything-gzip-ssrf
Open

fix(everything): block SSRF to internal/metadata IPs in gzip-file-as-resource#4498
olaservo wants to merge 6 commits into
modelcontextprotocol:mainfrom
olaservo:fix/everything-gzip-ssrf

Conversation

@olaservo

@olaservo olaservo commented Jul 8, 2026

Copy link
Copy Markdown
Member

Description

Adds SSRF protection to the everything server's gzip-file-as-resource tool. The tool fetched a caller-supplied http/https URL and returned the (compressed) response as a resource. Its only host restriction was an optional domain allowlist (GZIP_ALLOWED_DOMAINS) that is empty by default and treated as "all domains allowed", with no IP-range filtering and automatic redirect following. Because the URL is model-produced and prompt-injection-steerable, the server could be driven to fetch loopback, private, link-local, and cloud-metadata endpoints (e.g. 169.254.169.254) and return their contents to the caller.

Server Details

  • Server: everything (TypeScript, @modelcontextprotocol/server-everything)
  • Changes to: tools (gzip-file-as-resource)

Motivation and Context

validateDataURI allowed http/https/data and enforced only the domain allowlist (skipped entirely when empty), and fetchSafely called fetch(url) with default redirect following and no loopback/RFC1918/link-local/metadata checks.

What this PR does:

  • Adds assertPublicHost() plus IPv4/IPv6 classifiers that resolve the destination host and refuse non-public addresses (loopback, private/RFC1918, link-local/metadata, ULA, multicast, reserved, unspecified), covering IPv4, IPv6, and IPv4-mapped IPv6 (including the hex form the WHATWG URL parser normalizes to).
  • Replaces automatic redirect following with fetchWithGuardedRedirects (redirect: "manual"), re-validating the host on every hop and refusing redirects to non-http(s) schemes.
  • The IP guard applies regardless of GZIP_ALLOWED_DOMAINS. The domain-allowlist semantics are intentionally left unchanged (empty still means "all domains"), so the tool's default demo URL keeps working; the allowlist and the SSRF guard are independent controls.

How Has This Been Tested?

  • New unit tests assert the tool refuses non-public hosts (loopback, 169.254.169.254, RFC1918 ranges, 0.0.0.0, IPv6 loopback, IPv4-mapped IPv6) via IP literals (no network required).
  • npm run build (tsc) passes; full vitest suite passes (115 tests, including the 8 new cases).
  • Existing data: URI and public-URL behavior is unchanged and still covered by tests.

Breaking Changes

No client configuration changes required. Behavior only changes for requests that targeted non-public IPs, which are now refused (the intended fix). data: URIs and public URLs are unaffected.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follows MCP security best practices
  • I have updated the server's README accordingly
  • I have tested this with an LLM client
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have documented all environment variables and configuration options

Additional context

Docs updated in docs/instructions.md and docs/structure.md to note that internal/metadata IPs are always blocked (and re-validated across redirects) independent of the allowlist. As with any resolve-then-connect guard, a narrow DNS-rebinding window remains; pinning to the validated IP could be added later.

🤖 Generated with Claude Code

Prior art

The gzip-file-as-resource tool and its original GZIP_ALLOWED_DOMAINS domain-allowlist SSRF mitigation were added in #2831 (@ochafik). This PR builds on that baseline with an IP-range guard that applies independently of the domain allowlist.

olaservo and others added 2 commits July 8, 2026 07:43
…resource

The gzip-file-as-resource tool fetched a caller-supplied URL with only an
optional domain allowlist (empty by default, treated as allow-all) and no
IP-range filtering, and followed redirects without re-validation. A
prompt-injection-steered URL could drive the server to fetch loopback,
private, link-local, and cloud-metadata endpoints (e.g. 169.254.169.254)
and return their contents to the caller.

Resolve the destination host and refuse non-public IP addresses (loopback,
private/RFC1918, link-local/metadata, ULA, multicast, reserved,
unspecified), covering IPv4, IPv6, and IPv4-mapped IPv6, and follow
redirects manually so every hop is re-validated. This applies regardless of
GZIP_ALLOWED_DOMAINS, whose domain-allowlist semantics are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unwrap deprecated IPv4-compatible IPv6 addresses (::a.b.c.d, ::/96)
and classify them as IPv4, so forms like [::127.0.0.1] are refused
rather than treated as public. Adds test coverage for the
IPv4-compatible form and for carrier-grade NAT (100.64.0.0/10).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the everything server’s gzip-file-as-resource tool against SSRF by adding IP-range blocking (for loopback/private/link-local/metadata/reserved ranges) and by replacing automatic redirect following with manual redirects that re-validate the destination host at each hop.

Changes:

  • Added host resolution + IPv4/IPv6 (incl. IPv4-mapped IPv6) classifiers to block non-public destinations for http(s) fetches.
  • Implemented manual redirect following with per-hop SSRF re-validation and refusal of redirects to non-http(s) schemes.
  • Updated server docs and added unit tests for blocked IP literals.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/everything/tools/gzip-file-as-resource.ts Adds SSRF host/IP validation and guarded manual redirect following for fetches.
src/everything/docs/structure.md Documents SSRF blocking behavior for the gzip tool.
src/everything/docs/instructions.md Notes SSRF blocking (including across redirects) in constraints section.
src/everything/tests/tools.test.ts Adds tests ensuring non-public IP literal targets are refused.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/everything/tools/gzip-file-as-resource.ts
Comment thread src/everything/__tests__/tools.test.ts
… guard

Addresses Copilot review feedback on modelcontextprotocol#4498:
- assertPublicHost now races the dns/promises lookup against the fetch
  AbortSignal via a withAbort helper, so a slow-walking resolver can no
  longer exceed the tool's documented timeout (DNS was previously
  awaited outside the AbortSignal).
- Add a test that a public URL redirecting to 169.254.169.254 is
  refused at the second hop before any request is made to the internal
  target, guarding against regressions in per-hop re-validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread src/everything/tools/gzip-file-as-resource.ts
Addresses Copilot feedback on modelcontextprotocol#4498: restructure the guarded-redirect
loop to check GZIP_MAX_REDIRECTS after detecting a redirect but before
resolving/validating/following the next hop. Behavior is unchanged (at
most GZIP_MAX_REDIRECTS redirects followed), but the final redirect
target is no longer parsed and assigned before the loop throws.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread src/everything/tools/gzip-file-as-resource.ts
Comment thread src/everything/__tests__/tools.test.ts
Addresses Copilot feedback on modelcontextprotocol#4498:
- fetchWithGuardedRedirects now cancels each redirect Response body
  before following the next hop, so undici can release the socket
  instead of leaving it pinned across the redirect chain.
- Add tests that mock dns/promises.lookup to cover the hostname
  resolution branch of assertPublicHost: a hostname resolving to the
  metadata IP is refused, and the "any resolved address blocked" rule
  is exercised with a mixed public/private result set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread src/everything/tools/gzip-file-as-resource.ts Outdated
Addresses Copilot feedback on modelcontextprotocol#4498: assertPublicHost now catches
dns/promises.lookup failures (ENOTFOUND/EAI_AGAIN, or the abort-signal
timeout) and rethrows with the host and URL included, so resolution
errors are consistent with the tool's other wrapped errors instead of
surfacing Node's raw message without context.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants