Skip to content

security(mcp): migrate to rmcp 2 and enforce Host and Origin validation - #29

Merged
vietanhdev merged 1 commit into
mainfrom
security/rmcp-upgrade
Jul 19, 2026
Merged

security(mcp): migrate to rmcp 2 and enforce Host and Origin validation#29
vietanhdev merged 1 commit into
mainfrom
security/rmcp-upgrade

Conversation

@vietanhdev

Copy link
Copy Markdown
Owner

Clears RUSTSEC-2026-0189 (CVSS 8.8). Verified: cargo audit passes without the exemption, which is removed from ci.yml rather than left in place.

What was actually wrong

The advisory names rmcp's Streamable HTTP transport, and this app used SSE — so it was flagged on version, not reachability. I said so at the time rather than overstating it.

But the underlying weakness was real and lived in our code: no Host or Origin validation anywhere, so a page you visited could POST to 127.0.0.1 and invoke tools. rmcp 0.1.5 offered no seam to add eitherSseServer built its axum router internally and App was private. That, not the advisory, is why the upgrade was necessary.

Why this is a transport migration

rmcp 2 removed the SSE server transport entirely. Streamable HTTP is also where the advisory's fix landed, which is what makes the checks available:

  • allowed_hosts defaults to loopback → closes DNS rebinding.
  • allowed_origins defaults to EMPTY, and empty means Origin validation is DISABLED. It has to be set explicitly or the cross-origin hole stays open.

That second point is the one worth dwelling on. A Host check alone is not enough: a page can fetch the loopback endpoint with mode: 'no-cors', the Host header matches because the request really is going to 127.0.0.1, and only the Origin reveals another site sent it.

Listing only our own origins keeps genuine MCP clients working — they aren't browsers and send no Origin at all — while anything from a browser tab is rejected unless truly same-origin.

Tests

Both allowlists are built by named functions with tests, because every failure here is silent:

Test Failure it catches
allowed_origins_is_never_empty Emptying the list turns validation off while everything still appears to work
allowed_origins_carry_a_scheme_and_the_right_port rmcp matches per RFC 6454 on (scheme, host, port); a schemeless entry matches nothing
allowed_hosts_cover_the_port_qualified_forms A browser sends Host: 127.0.0.1:8779, not a bare host
allowed_hosts_are_loopback_only A routable host would undo the bind-time restriction
a_foreign_origin_is_not_allowed The threat itself

API changes

#[tool(tool_box)]#[tool_router], #[tool(aggr)]Parameters<T>. ServerHandler is written out rather than generated by tool_router(server_handler), which would have silently dropped the instructions the client shows its model. StreamableHttpServerConfig and ServerInfo are both non-exhaustive now, so both use their builder/Default.

⚠️ Breaking for existing MCP clients

The endpoint moves /sse/mcp and the transport ssehttp. Docs, the AI Integration view and the copy-paste client configs are updated with a warning explaining both.

Not verified here

The container launch test lives on the gui-launch branch and isn't on this one. Build, clippy, the full suite and cargo audit all pass; end-to-end launch verification happens when the branches meet.

Clears RUSTSEC-2026-0189 (CVSS 8.8) -- verified: cargo audit passes
without the exemption, which is now removed from ci.yml rather than left
in place.

The advisory names rmcp's Streamable HTTP transport, and this app used
SSE, so it was flagged on version rather than reachability. The
underlying weakness was real regardless and lived in our code: no Host or
Origin validation anywhere, so a page the user visited could POST to
127.0.0.1 and invoke tools. rmcp 0.1.5 offered no seam to add either --
SseServer built its axum router internally and App was private.

rmcp 2 removed the SSE server transport entirely, so this is a transport
migration as well as a version bump. Streamable HTTP is also where the
advisory's fix landed, which is what makes the checks available:

  allowed_hosts defaults to loopback and closes DNS rebinding.

  allowed_origins defaults to EMPTY, and empty means Origin validation is
  DISABLED. It has to be set explicitly or the cross-origin hole stays
  open. A Host check alone is not enough -- a page can fetch loopback with
  mode:'no-cors', the Host header matches because the request really is
  going to 127.0.0.1, and only the Origin reveals another site sent it.

Listing only our own origins keeps genuine MCP clients working: they are
not browsers and send no Origin at all, while anything from a browser tab
is rejected unless truly same-origin.

Both lists are built by named functions with tests, because the failure
mode is silent: an emptied allowed_origins turns validation off while
everything still appears to work, and a schemeless entry matches nothing
under RFC 6454.

API changes: #[tool(tool_box)] -> #[tool_router], #[tool(aggr)] ->
Parameters<T>. ServerHandler is written out rather than generated by
tool_router(server_handler), which would have silently dropped the
instructions the client shows its model. StreamableHttpServerConfig and
ServerInfo are both non-exhaustive now, so both are built via their
builder/Default rather than struct literals.

BREAKING for existing MCP clients: the endpoint moves from /sse to /mcp
and the transport from sse to http. Docs, the AI Integration view and the
copy-paste client configs are all updated, with a warning explaining both
changes.

Not verified by the launch test here -- that script lives on the
gui-launch branch and is not on this one yet. Build, clippy, the full
suite and cargo audit all pass.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
thinkutils 4eead03 Jul 19 2026, 03:33 PM

@vietanhdev
vietanhdev merged commit 386d9f5 into main Jul 19, 2026
7 of 8 checks passed
@vietanhdev
vietanhdev deleted the security/rmcp-upgrade branch July 19, 2026 16:17
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.

1 participant