8000 Add support for DNS rebinding protections by ddworken · Pull Request #861 · modelcontextprotocol/python-sdk · GitHub
[go: up one dir, main page]

Skip to content

Add support for DNS rebinding protections #861

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

ddworken
Copy link
Contributor

Motivation and Context

This implements the mitigations described here. To avoid breaking existing applications this doesn't enable any changes by-default, but enabling this feature is heavily encouraged for any local MCP servers using the SSE transport.

How Has This Been Tested?

Tested via unit tests.

Breaking Changes

To avoid introducing any breaking changes, the DNS rebinding protections are disabled by default. Ideally we should find a way to enable them by default. But for now, adding them as disabled is a good first step.

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 Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@ddworken ddworken marked this pull request as ready for review May 30, 2025 17:01
Copy link
Member
@jerome3o-anthropic jerome3o-anthropic left a comment

Choose a reason for hiding this comment

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

one small change

@Kludex
Copy link
Member
Kludex commented Jun 11, 2025

Why not passing those values to uvicorn instead of creating a new class, and a whole flow for it?

Uvicorn does support the proposed settings.

@ddworken
Copy link
Contributor Author

Why not passing those values to uvicorn instead of creating a new class, and a whole flow for it? Uvicorn does support the proposed settings.

Can you point me to where this is supported? I looked around and can't find anything where uvicorn supports checks like this. It is also worth noting that the Origin checking is a bit specialized and is different from CORS checks since we need to actually reject requests with non-matching origin headers (whereas your average CORS middleware doesn't reject requests, it just doesn't set CORS response headers [example]).

@Kludex
Copy link
Member
Kludex commented Jun 11, 2025

Isn't what you want the --forwarded-allow-ips?

You can check the implementation here: https://github.com/encode/uvicorn/blob/master/uvicorn/middleware/proxy_headers.py

The forwarded_allow_ips becomes the trusted_ips.

@ddworken
Copy link
Contributor Author

No, sadly that doesn't do what we want. DNS rebinding risks don't have anything to do with proxies or the x-forwarded-for header. We specifically need to reject any requests where the incoming Host and Origin headers don't match an allowlist. This blog has a good write up on the risks here and why checking the Host header mitigates them.

@Kludex
Copy link
Member
Kludex commented Jun 12, 2025

Ah, this problem is for: "local MCP servers using the SSE transport."

Since the "problem" is also present when using uvicorn standalone, I'd say this adds unnecessary complexity to the project and that the official recommendation should be to not run it locally, or to run behind a well tested reverse proxy. There are too many features already implemented in nginx that it will just a lot of complexity if this project implements it.

Are the other SDKs implementing this?

@randomstuff
Copy link
randomstuff commented Jun 12, 2025

should be to not run it locally

AFAIU, "I run locally" is the main use case of MCP according to the architecture diagram, though.

or to run behind a well tested reverse proxy

If the application is ran locally, spawned by some local MCP host it is typically not going to be run behind a reverse proxy.

Since the "problem" is also present when using uvicorn standalone

(On this topic, I think it might be helpful for application web servers like uvicorn or for web framework like starlette to provide some built-in or blessed option for providing DNS-rebinding protection.)


Note that even if DNS rebinding protection is used, the service can still be attacked by other local users. This might be important on multi-user systems for example. A better solution is to use the stdio transport or to serve the HTTP transport over Unix Domain Socket.

If the service is hosted on a server machine, a localhost-bound MCP server without proper authentication could be attacked by a compromised local process.

In both cases, it might be safer to use UDS-bound services if possible.

@Kludex
Copy link
Member
Kludex commented Jun 12, 2025

I agree with @randomstuff.


(On this topic, I think it might be helpful for application web servers like uvicorn or for web framework like starlette to provide some built-in or blessed option for providing DNS-rebinding protection.)

(Given the nature of this attack, I don't think there's a compelling case to add it on Uvicorn - also, in 8 years of its existence no one ever requested it) [I maintain both Uvicorn and Starlette].

@randomstuff
Copy link

(Slightly out topic)

(Given the nature of this attack, I don't think there's a compelling case to add it on Uvicorn - also, in 8 years of its existence no one ever requested it) [I maintain both Uvicorn and Starlette].

I suspect DNS-rebinding vulnerability is still highly prevalent for embedded and localhost-bound services (including development environment). No developer ever adds DNS-rebinding protection logic in their application in order to make sure their local machine cannot be attacked during development for example.

This is by large mitigated by the fact that Chromium nowadays includes some DNS-rebinding protection and because virtually anyone uses Chromium-based browsers anyway. For the rest of us still not using Chromium-based browsers, this is still a potential attack vector AFAIU (apparently largely under-exploited in practice), often yielding to high values assets/high impact (cryptocurrency, RCE, etc.). Having some blessed recommendation on how to avoid this attack in major frameworks and servers might help mitigate this issue. A --accepted-hosts parameter might be a nice addition to uvicorn, flask CLIs.

return Response("Invalid Content-Type header", status_code=400)

# Skip remaining validation if DNS rebinding protection is disabled
if not self.settings.enable_dns_rebinding_protection:

Choose a reason for hiding this comment

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

It might make sense to reject the request for DNS rebinding protection before other checks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking at this code, I think it is in a reasonable order. I don't see any weaknesses exposed by doing it in this order. Do you?

Choose a reason for hiding this comment

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

I don't believe there is any significant difference in term of security.

It is just that logically speaking it would make sense to always return 421 when the Host is incorrect, for any request, even if the request path does no match any route for example (http::/unxpectedhost/unspectedpath should return 421 instead of 404). (Somewhat nitpicky)

(What could be event better in term of DNS-rebinding protection would be to drop the connection altogether when the host is invalid. I guess that doing so DNS-rebinding could not even be used for fingerprinting (think something like the Facebook localhost scanning thing). I think some reverse proxies may support that when returning some special status code but I don't think it is possible with portable ASGI.)

@ddworken
Copy link
Contributor Author

Are the other SDKs implementing this?

I believe so. The spec has been updated to recommend this so I believe other SDKs should also offer this as an opt-in protection.

I suspect DNS-rebinding vulnerability is still highly prevalent for embedded and localhost-bound services (including development environment). No developer ever adds DNS-rebinding protection logic in their application in order to make sure their local machine cannot be attacked during development for example.

+1, I do see DNS rebinding as something that is often missed since it mainly impacts local development machines, but as something that is still quite impactful. So to me, it does seem like a reasonable thing for frameworks to implement since I always see it as better for a framework to provide security, rather than developers having to implement it for individual services.

This is by large mitigated by the fact that Chromium nowadays includes some DNS-rebinding protection and because virtually anyone uses Chromium-based browsers anyway. For the rest of us still not using Chromium-based browsers, this is still a potential attack vector AFAIU (apparently largely under-exploited in practice), often yielding to high values assets/high impact (cryptocurrency, RCE, etc.). Having some blessed recommendation on how to avoid this attack in major frameworks and servers might help mitigate this issue. A --accepted-hosts parameter might be a nice addition to uvicorn, flask CLIs.

While it is true that Chromium provides some mitigations (as do some DNS providers), it is worth emphasizing these are pretty incomplete and have known bypasses. So personally I wouldn't recommend ever relying solely on those mitigations for anything critical.

@jerome3o-anthropic jerome3o-anthropic self-requested a review June 17, 2025 14:28
Copy link
Member
@jerome3o-anthropic jerome3o-anthropic left a comment

Choose a reason for hiding this comment

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

I believe this is all needed based on the conversations in this PR and my understanding of the vector.

It's only relevant to running sse/shttp on local host, which probably (never say never) only happens during development. iiuc this isn't the default behaviour so it's depending on the dev opting in, which feels like it wont happen very often. I wonder if we're really concerned about this vuln we should make it the default when running on localhost or 127.0.0.1?

Either way this is a step toward mitigation

@ddworken
Copy link
Contributor Author

I think making it the default is probably the right answer in the long term, but that is technically a breaking change so I would rather do that in a separate PR. I also suspect that if we want to do that, we should probably try to coordinate that across all the different SDKs at the same-ish time to unify communications.

@jerome3o-anthropic jerome3o-anthropic merged commit 86bb54c into modelcontextprotocol:main Jun 17, 2025
9 checks passed
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.

4 participants
0