Skip to content

feat(dns-rebinding): check Host and Origin validation separately - #575

Open
ninadphalak wants to merge 3 commits into
modelcontextprotocol:mainfrom
ninadphalak:server-dns-rebinding-split-checks
Open

ninadphalak wants to merge 3 commits into
modelcontextprotocol:mainfrom
ninadphalak:server-dns-rebinding-split-checks

Conversation

@ninadphalak

Copy link
Copy Markdown

Closes #535.

localhost-host-rebinding-rejected forges Host and Origin in the same request and accepts any 4xx, so a server that validates only one of the two headers passes. This adds two checks to the dns-rebinding-protection scenario so each header is tested on its own:

Check Request Expects Miss
localhost-host-only-rebinding-rejected non-localhost Host, no Origin any 4xx WARNING (no spec text requires Host validation)
localhost-origin-only-rebinding-rejected localhost Host, non-localhost Origin exactly 403 FAILURE (Streamable HTTP: "If the Origin header is present and invalid, servers MUST respond with HTTP 403 Forbidden")

localhost-host-rebinding-rejected and localhost-host-valid-accepted are unchanged.

Examples

  • The everything-server now returns 403 for a present, non-localhost Origin (createMcpExpressApp() validates Host only).
  • Two negative fixtures, dns-rebinding-host-only.ts and dns-rebinding-origin-only.ts. Each passes the combined check and fails its own split check; negative.test.ts asserts both.
  • no-dns-rebinding-protection.ts used an invalid inputSchema and returned 500; it now uses a zod schema (separate commit).

Spec follow-up. modelcontextprotocol/modelcontextprotocol#3370 proposes rewording Host/Origin validation (Host for DNS rebinding, Origin for CSRF). It has no maintainer reply yet. The expected Origin status and both miss severities sit in one block in dns-rebinding.ts, so they can follow #3370 with a small change if it lands.

Checked

The echo tool declared inputSchema as a plain JSON Schema object, which
@modelcontextprotocol/sdk 1.29 rejects, so the fixture answered every
request with HTTP 500. The dns-rebinding-protection negative test passed
on that 500 instead of on the missing protection. With z.string() the
fixture serves the forged request with 200, as intended.
localhost-host-rebinding-rejected forges Host and Origin in one request
and accepts any 4xx, so a server that validates only one of the headers
passes. Add two checks to the same scenario:

- localhost-host-only-rebinding-rejected: non-localhost Host, no Origin,
  expects any 4xx. No spec text requires Host validation, so a miss is
  WARNING.
- localhost-origin-only-rebinding-rejected: localhost Host, non-localhost
  Origin, expects exactly 403 per the Streamable HTTP transport ("If the
  Origin header is present and invalid, servers MUST respond with HTTP
  403 Forbidden"). A miss is FAILURE.

The combined check and localhost-host-valid-accepted are unchanged. The
expected status and both miss severities sit in one block so they can
follow modelcontextprotocol/modelcontextprotocol#3370.

The everything-server now returns 403 for a present, non-localhost
Origin; createMcpExpressApp() validates Host only. Two negative fixtures,
dns-rebinding-host-only.ts and dns-rebinding-origin-only.ts, show each
partial setup passing the combined check and failing its split check.
The Host-only and Origin-only checks pass when the forged request is
rejected, so a server that rejects every request passed both without
validating either header.

Send a control request first (localhost Host, no Origin). If the server
does not answer it with 2xx, report both checks as not testable via
untestableCheck(), at their usual severity (Host-only WARNING,
Origin-only FAILURE). The combined check and localhost-host-valid-accepted
are unchanged.

Adds a negative test with a server that answers 403 to everything.

Control request idea from @JosephDoUrden in modelcontextprotocol#535.

This branch has not been deployed

No deployments
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.

dns-rebinding-protection: check Host and Origin validation separately

1 participant