Skip to content

fix(http): add origin/host check#764

Open
jokemanfire wants to merge 1 commit intomodelcontextprotocol:mainfrom
jokemanfire:sec
Open

fix(http): add origin/host check#764
jokemanfire wants to merge 1 commit intomodelcontextprotocol:mainfrom
jokemanfire:sec

Conversation

@jokemanfire
Copy link
Member

This is a security problem ,there is a lack of cross-domain access verification here, which may be exploited by malicious attacks.

Motivation and Context

Adds Host/Origin validation to the Streamable HTTP server to mitigate DNS rebinding risks. Introduces configurable allowed hosts, rejects invalid or mismatched request origins, and adds integration tests for both valid localhost traffic and malicious request scenarios.

How Has This Been Tested?

Add the it test

Breaking Changes

Yes, the sec change may change the mcp's server feature which has been deployed.

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

@jokemanfire jokemanfire requested a review from a team as a code owner March 20, 2026 09:39
Copilot AI review requested due to automatic review settings March 20, 2026 09:39
@github-actions github-actions bot added T-test Testing related changes T-core Core library changes T-transport Transport layer changes labels Mar 20, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds DNS rebinding mitigations to the Streamable HTTP server by validating inbound Host/Origin headers against a configurable allowlist, and introduces an integration test covering allowed vs. blocked host/origin scenarios.

Changes:

  • Add allowed_hosts to StreamableHttpServerConfig with helpers to configure/disable it.
  • Enforce Host/Origin validation early in request handling, returning 403 Forbidden on violations.
  • Add an integration test asserting 403 for disallowed Host and disallowed Origin.

Reviewed changes

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

File Description
crates/rmcp/src/transport/streamable_http_server/tower.rs Introduces allowlisted Host/Origin validation for DNS rebinding protection and config knobs to control it.
crates/rmcp/tests/test_custom_headers.rs Adds an integration test to verify Host/Origin enforcement behavior.

Comment on lines +52 to +56
///
/// By default, Streamable HTTP servers only accept loopback hosts to
/// prevent DNS rebinding attacks against locally running servers. Public
/// deployments should override this list with their own hostnames.
pub allowed_hosts: Vec<String>,
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

Adding the new pub allowed_hosts field to StreamableHttpServerConfig is a semver-breaking change for downstream users constructing the config via struct literals (they will now fail to compile). If this is intended, it should be paired with a major version bump / explicit release note; otherwise consider making the config #[non_exhaustive] and/or moving toward a constructor/builder pattern to avoid future breakage.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

This is especially important given that PR #715 was recently merged to prepare for 1.0 stable release. StreamableHttpServerConfig may have been missed in that effort. Adding #[non_exhaustive] here would be consistent with that direction and prevent this class of breakage going forward.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this security update should be introduced in 1.0 version , and I will add the #[non_exhaustive].

Comment on lines +944 to +948
let bad_origin_request = Request::builder()
.method(Method::POST)
.header("Accept", "application/json, text/event-stream")
.header(CONTENT_TYPE, "application/json")
.header("Host", "localhost:8080")
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

Consider adding a test case for the same-hostname/different-port scenario (e.g., Host: localhost:8080 with Origin: http://localhost:9999) and assert it is rejected. This guards against port-mismatch bypasses of the Host/Origin validation logic.

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +190
Ok(Some(normalize_host(authority.host())))
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

parse_host_header/parse_origin_host currently discard the port by extracting only authority.host(). That makes the later Host/Origin comparison effectively “hostname-only”, allowing Host: localhost:8080 with Origin: http://localhost:9999 to pass. Consider preserving and comparing the full authority (host + optional port), or at least requiring ports to match when present (accounting for default ports).

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Great

Copy link
Member

@DaleSeo DaleSeo left a comment

Choose a reason for hiding this comment

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

Thank you for tackling DNS rebinding protection, @jokemanfire. This is an important security hardening that the MCP Streamable HTTP transport was missing.

I actually implemented a similar Host validation feature on the consumer side in apollographql/apollo-mcp-server#602, so I very much welcome this change. Once this lands in the SDK, I'd love to remove our application-layer middleware and rely on this built-in protection instead.

I have a few suggestions based on what I learned building the consumer-side implementation.


let origin = parse_origin_host(headers)?;
if let Some(origin) = origin.as_deref() {
if !host_is_allowed(origin, &config.allowed_hosts) {
Copy link
Member

@DaleSeo DaleSeo Mar 22, 2026

Choose a reason for hiding this comment

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

Is it intentional to checking Origin against allowed_hosts here? allowed_hosts answers "what hostnames is this server known as?" while Origin represents the source page that initiated the request. These are different concepts.

For example, consider an MCP server at mcp.example.com being called by a frontend at app.example.com:

allowed_hosts: ["mcp.example.com"]

Host: mcp.example.com           // ✅ this IS the server
Origin: http://app.example.com  // ❌ rejected, but this is a legitimate caller

From my understanding, restricting which origins can call the server is a CORS concern. For DNS rebinding protection, validating only the Host header should be sufficient.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well ,thanks ,I will take some time to resolve this.

));
}
if let Some(host) = host.as_deref() {
if origin != host {
Copy link
Member

Choose a reason for hiding this comment

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

The Host header identifies the destination server while Origin identifies the source page. They will naturally differ in any cross-origin scenario.

Would you consider removing the Origin validation entirely from this DNS rebinding guard and leaving origin-based restrictions to CORS middleware where they belong?

Comment on lines +134 to +138
fn normalize_host(host: &str) -> String {
host.trim_matches('[')
.trim_matches(']')
.to_ascii_lowercase()
}
Copy link
Member

Choose a reason for hiding this comment

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

normalize_host does not preserve the port, and parse_host_header uses authority.host(), which also strips the port. As a result, both localhost:8000 and localhost:9999 normalize to localhost and pass validation equally. This is significant because multiple services can run on different ports of the same host. Without port validation, a DNS rebinding attack could target a different service on localhost:9999, and the check would pass since localhost is in the allow list. Would you consider adding similar port-aware validation here?

Copy link
Member Author

@jokemanfire jokemanfire Mar 23, 2026

Choose a reason for hiding this comment

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

Yes , It should be considerd.

}
}

Ok(())
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to reject requests that are missing the Host header when allowed_hosts is non-empty?

TypeScript SDK's DNS rebinding guard rejects requests with missing headers as well: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/server/src/server/middleware/hostHeaderValidation.ts

Copy link
Member Author

Choose a reason for hiding this comment

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

Keep same with ts.

Comment on lines +52 to +56
///
/// By default, Streamable HTTP servers only accept loopback hosts to
/// prevent DNS rebinding attacks against locally running servers. Public
/// deployments should override this list with their own hostnames.
pub allowed_hosts: Vec<String>,
Copy link
Member

Choose a reason for hiding this comment

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

This is especially important given that PR #715 was recently merged to prepare for 1.0 stable release. StreamableHttpServerConfig may have been missed in that effort. Adding #[non_exhaustive] here would be consistent with that direction and prevent this class of breakage going forward.

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

Labels

T-core Core library changes T-test Testing related changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants