Skip to content

fetch: harden URL fetching against private-network access, oversized responses, and redirect/DNS gaps #4116

Closed as not planned

Description

@lcv-leo

Summary

While auditing and hardening local MCP server usage, I reviewed the current mcp-server-fetch behavior and built a local hardened wrapper for operational use. I am opening this issue to share concrete findings and implementation ideas that may be useful upstream.

This is not a report about a known exploit in a specific deployment. It is a defense-in-depth report about the default risk profile of a fetch tool exposed to autonomous agents.

Package/runtime reviewed

  • Package: [email protected]
  • Runtime shape observed locally: uvx mcp-server-fetch
  • Source evidence reviewed locally: server.py uses AnyUrl, checks robots.txt by default, calls httpx.AsyncClient().get(..., follow_redirects=True, timeout=30), then reads response.text and applies output slicing.
  • Audit check: pip-audit over a uvx --with mcp-server-fetch environment returned no known package CVEs at the time of testing.

Main findings

  1. Private/internal-network access is not blocked by default.
    A URL fetch tool can become an SSRF-style primitive when made available to an LLM/agent. The official docs already warn that the server can access local/internal IPs, but safer defaults would reduce accidental exposure.

  2. Redirects need the same policy checks as initial URLs.
    With follow_redirects=True, a public URL can redirect to loopback, RFC1918, link-local, metadata, or other reserved ranges unless every hop is validated.

  3. Output max_length is not a transport byte cap.
    The observed implementation reads response.text before slicing output. Large responses can still consume memory/CPU/network before truncation.

  4. DNS rebinding / TOCTOU risk.
    If a hostname is checked separately from the actual connection target, DNS answers can change between validation and connect. At minimum, the implementation should resolve and reject unsafe answers before connecting; stronger implementations should pin or otherwise constrain the connection target.

  5. Structured metadata would help agents reason safely.
    Current outputs are easier to use if they include final URL, status, content type, bytes read, truncation status, robots decision, and redirect chain.

  6. Windows extraction/runtime clarity.
    On Windows, I observed a readabilipy warning that Node/NPM was not found in the uvx environment, so it fell back to Python extraction. This is not fatal, but it is useful to document because it affects extraction behavior and user expectations.

Local hardening behavior that worked well

For our local wrapper, we kept the expected fetch tool name and added these guards:

  • allow only http: and https: schemes
  • reject embedded URL credentials
  • optional allowlist and denylist host controls
  • DNS-resolve before requests and reject loopback, RFC1918, link-local, carrier-grade NAT, documentation/test ranges, multicast/reserved ranges, and IPv6 local/link-local equivalents
  • validate every redirect target before following it
  • enforce a pre-read byte cap during streaming, not only output slicing
  • retain robots.txt checks by default
  • return structured metadata: url, final_url, status, content_type, bytes_read, truncated, robots, redirects

Validation from local wrapper

Smoke output from the hardened local wrapper:

fetch tools: fetch
fetch example.com status=200 bytes=528 final=https://example.com/
fetch file block: Blocked unsupported URL scheme: file:
fetch loopback block: Blocked private or reserved IP address: 127.0.0.1

Suggested upstream changes

  • Add default private/reserved IP blocking with an explicit opt-out env var.
  • Re-validate every redirect hop before following it.
  • Add a streaming byte cap before decoding text.
  • Reject embedded URL credentials.
  • Add optional FETCH_ALLOW_HOSTS / FETCH_DENY_HOSTS style controls.
  • Return structured metadata alongside text content.
  • Document Windows extraction behavior and recommended PYTHONIOENCODING=utf-8/PATH considerations.

Compatibility note

These changes can be mostly additive if implemented behind conservative config names and with a clear opt-out for users who intentionally need local/private-network access.

Activity

  1. policylayer-dan commented on May 6, 2026

    @policylayer-dan

    The SSRF framing is correct and the finding is well-scoped. A few things worth separating:

    Redirect and DNS rebinding are particularly hard to fix purely at the server layer because the dangerous resolution happens after your initial check. A URL that passes an allowlist check at request time can resolve to a private IP after a DNS change or mid-redirect. Server-side fixes help but aren't fully reliable on their own.

    The layering that actually closes the gap: an allowlist enforced at the tool-call layer (before the request is even dispatched to the fetch server) combined with scoped credentials so the agent can't reach internal services even if a URL slips through. That's distinct from what the server itself can enforce.

    For the response-size finding, a hard cap in the server is the right place for that - it's a pure server concern. The private-network access issue is more naturally handled as a URL allowlist enforced by the consumer before the call is made. If you're running mcp-server-fetch alongside other servers you don't control, that consumer-layer allowlist is what PolicyLayer enforces deterministically across the fleet.

  2. sophymarine commented on May 7, 2026

    @sophymarine

    The hardening direction here is right, especially the private-network and redirect boundaries. One thing that tends to get missed is how the failure mode should surface to the client: a clear, structured error for blocked targets plus a separate timeout class makes retries much easier to reason about. If you add a lightweight allowlist/denylist test matrix alongside the URL validation tests, you can lock in the security behavior without making the happy path brittle. That also helps downstream server implementers avoid reintroducing SSRF regressions when they adapt the fetch tool.

  3. lcv-leo commented on May 7, 2026

    @lcv-leo
    Author

    Thanks for the thoughtful notes. I agree with the layering distinction: consumer-side tool-call policy/allowlisting is complementary and important, but this issue is intentionally scoped to safer defaults in the reference fetch server itself.

    For this server-side layer, I think the actionable pieces remain:

    • deterministic blocked-target handling with clear structured errors;
    • explicit timeout/error classes so callers can reason about retries;
    • regression tests for allow/deny, redirect, private-network, and response-size cases;
    • safe defaults that reduce risk even when the caller layer is thin.

    Consumer-layer enforcement is valuable defense in depth, but I would treat it as complementary rather than a replacement for server-side hardening here.

  4. SyedAnas01 commented on May 22, 2026

    @SyedAnas01

    mcp-safeguard SSRF rules (SS-001–SS-003) now detect this

    Dropping a note that mcp-safeguard (pip install mcp-safeguard) v0.3.0 added SSRF static analysis rules that catch the vulnerability described here:

    • SS-001: URL parameter without allowlist/blocklist protection
    • SS-002: Blind URL fetch with no scope restriction in description
    • SS-003: Redirect following without revalidation risk

    These rules fire on mcp-server-fetch, playwright-mcp, and any MCP tool accepting unconstrained URL parameters. Useful as a CI gate to prevent SSRF-vulnerable tools from shipping.

    One edge case relevant to this issue: when get_prompt is invoked (rather than the fetch tool), fetch_url() is called directly without any URL validation — the robots.txt check and any URL blocklist would need to be applied at the fetch_url() level (not just in call_tool) to cover this path.

    — Syed Anas Mohiuddin

  5. norika1207-lab commented on Jun 2, 2026

    @norika1207-lab
  6. lcv-leo commented on Jun 4, 2026

    @lcv-leo
    Author

    Thanks @norika1207-lab, I agree with framing this at the protocol boundary as well as the implementation boundary.

    A concrete acceptance-test matrix that would be useful here:

    • direct private-network target is blocked before returning content;
    • public URL redirecting to a private-network target is blocked;
    • oversized response is stopped before unbounded buffering;
    • timeout is distinguishable from policy denial;
    • blocked target returns a structured error that callers can branch on, not only a generic fetch failure.

    For each case, I would want the fixture to assert three signals together: the JSON-RPC/HTTP error sent to the peer, the local exception or cancellation reason visible to the server/host, and a compact trace/log field that ties the two together. That would complement the earlier allow/deny test matrix and make regressions much easier to diagnose downstream.

  7. localden commented on Jul 30, 2026

    @localden
    Contributor

    Closing as a duplicate of #3741; let's track it there.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions