← MCP server and skill grades

fetch

modelcontextprotocol/servers/src/fetch

B
Type
MCP server
Scanned
Oct 29, 2026
Version
194cc86e3ae6
Files read
25

This MCP server allows an AI agent to fetch web pages and convert their HTML contents into Markdown. The main risks are server-side request forgery (SSRF) via DNS rebinding or proxy resolution, and unsafe temporary file handling and unbounded subprocess execution when readabilipy invokes Node.js.

What it can reach

Network
Makes outbound HTTP and HTTPS requests (with optional HTTP or SOCKS5 proxy support) to any public URL requested by the agent or user.
Shell commands
By default, if Node.js is on the system PATH, the underlying readabilipy library spawns a Node.js subprocess (and potentially npm) to run Readability.js.
Files
When using the default Readability.js mode with Node.js installed, readabilipy reads and writes temporary files in the system temporary directory.

Findings

2 medium3 low
  1. MediumPrivate IP guard can be bypassed via DNS rebinding or proxy-only hostnames

    src/fetch/src/mcp_server_fetch/server.py:157

    The private-address guard resolves the target hostname in an httpx request hook before the request is sent, but httpx resolves the hostname a second time when opening the socket. An attacker controlling a domain with a short TTL can return a public IP during the check and a loopback or cloud metadata IP (such as 127.0.0.1 or 169.254.169.254) when httpx connects. Additionally, if local DNS resolution fails with socket.gaierror, the hook returns without error, allowing internal hostnames resolvable only by a configured corporate proxy to be fetched.

    Fix: Pin the resolved IP address at the custom transport/socket layer so the connection uses the exact IP address that passed validation, and fail closed on unresolvable hostnames unless explicitly configured otherwise.

  2. MediumDefault Readability.js integration uses predictable temp files and runs Node.js without a timeout

    src/fetch/src/mcp_server_fetch/server.py:174

    By default (use_readability_js=True), the server delegates HTML extraction to readabilipy with use_readability=True. As noted in the project's own test harness and documentation, readabilipy writes the fetched HTML and extracted output to fixed filenames (full.html and article.json) in the shared system temporary directory and spawns Node.js (and potentially npm) with no timeout. Concurrent fetches or another local user can overwrite or read these shared temporary files, and a hung Node.js process will block the server indefinitely.

    Fix: Set a per-instance isolated temporary directory using tempfile.TemporaryDirectory in production (as done in tests/harness.py), run the synchronous extraction in a worker thread with a timeout, or default to --no-readability-js.

  3. LowUnhiding hidden DOM elements exposes the agent to invisible prompt injection

    src/fetch/src/mcp_server_fetch/server.py:292

    To support server-side rendered pages that hide content until client hydration, _recover_hidden_content strips hidden, aria-hidden, display:none, and visibility:hidden attributes from any hidden container whose text is longer than the visible extracted text. Attackers commonly hide prompt injection payloads inside display:none or hidden blocks so human users cannot see them; this recovery logic causes those hidden blocks to be extracted and sent to the model.

    Fix: Make hidden-content recovery opt-in or restrict it to known hydration markers rather than unhiding arbitrary large display:none or hidden elements.

  4. LowEntire response body is downloaded into memory before length truncation

    src/fetch/src/mcp_server_fetch/server.py:477

    Although the fetch tool truncates the returned text to max_length (default 5000 characters), client.get() buffers the entire HTTP response body in memory and passes the full document through HTML parsing and Markdown conversion before slicing. A malicious URL serving a very large payload within the 30-second timeout can cause high memory and CPU usage.

    Fix: Use streaming responses (client.stream) and enforce a maximum response byte limit while reading the body.

  5. LowDockerfile creates a non-root user but runs the container as root

    src/fetch/Dockerfile:39

    The Dockerfile creates an unprivileged 'app' user and sets ownership of /app/.venv to app:app, but omits a USER app instruction before the ENTRYPOINT, causing the server process (and any Node.js subprocesses it spawns) to run as root inside the container.

    Fix: Add 'USER app' before the ENTRYPOINT instruction in the Dockerfile.