Skip to content

Allow the public host in MCP DNS-rebinding protection - #8

Merged
ebeigarts merged 1 commit into
mainfrom
bugfix/mcp-invalid-host-header
Aug 7, 2026
Merged

Allow the public host in MCP DNS-rebinding protection#8
ebeigarts merged 1 commit into
mainfrom
bugfix/mcp-invalid-host-header

Conversation

@ivarsb

@ivarsb ivarsb commented Aug 7, 2026

Copy link
Copy Markdown
Member

Problem

Users could not authenticate. Two symptoms, one cause:

  • Tool calls failed with {"error": "Authorization failed (403 Forbidden). Forbidden: Invalid Host header..."}
  • Connecting the connector failed with "Failed to complete OAuth authorization"

Root cause

No code change caused this — a transitive dependency did.

mcp 0.23.0 added DNS-rebinding protection to StreamableHTTPTransport: any Host header other than loopback (127.0.0.1, ::1, localhost) is rejected with 403 unless allow-listed via the new allowed_hosts: option.

Production installs this gem directly (gem install -N mail_mcp -v 1.0.2), which bypasses Gemfile.lock and resolves dependencies from the gemspec at image-build time. mcp "~> 0.14" floated up to 0.25.0 on a rebuild, and every authenticated /mcp call started 403ing.

The OAuth symptom is the same bug wearing a different hat: resolve_mcp_context halts with 401 before the transport is constructed, so unauthenticated discovery still worked — login form rendered, IMAP/SMTP validated, code exchanged, token issued. The connector's first authenticated initialize call then hit the 403 and reported it as an authorization failure.

Changes

  • lib/mail_mcp/app.rb — pass allowed_hosts: (derived from BASE_URL) to the transport
  • mail_mcp.gemspec — pin mcp to ~> 0.25.0. Not just a floor: because prod installs the gem directly, gemspec constraints are the production pins, and ~> 0.23 would leave the next mcp minor free to break this the same way
  • Gemfile.lockbundle update mcp → 0.25.0, so local/CI match prod
  • spec/mail_mcp/app_spec.rb/mcp specs send the real Host, plus coverage for host-with-port, a rebound host (403), and an end-to-end tools/call

Verification

Reproduced the exact production 403 in rspec first (rack-test defaults to Host: example.org), then confirmed against the real artifact: built the gem, installed it into a clean GEM_HOME (resolves mcp 0.25.0), ran mail_mcp server over HTTP.

Request Result
GET /health 200
Unauthenticated POST /mcp probe 401 + WWW-Authenticate
Authenticated initialize, public Host 200 (was 403)
Authenticated tools/list, rebound Host 403 — protection intact

84 specs pass, rubocop clean.

Deploying

Merging is not enough — the deploy pins an exact gem version. After merge: bump lib/mail_mcp/version.rb to 1.0.3, push to RubyGems, and update the deploy Dockerfile to gem install -N mail_mcp -v 1.0.3.

Follow-up (not in this PR)

/.well-known/oauth-authorization-server omits token_endpoint_auth_methods_supported, whose RFC 8414 default is client_secret_basic — but POST /oauth/token only reads params[:client_secret], never the Authorization header. Deliberately left out so it doesn't muddy this diagnosis. Worth checking if the connect flow still fails after release.

🤖 Generated with Claude Code

mcp 0.23.0 added DNS-rebinding protection to StreamableHTTPTransport: any
Host header other than loopback is rejected with 403 "Forbidden: Invalid
Host header" unless allow-listed via the new allowed_hosts: option.

Production installs this gem directly (`gem install mail_mcp -v X`), which
bypasses Gemfile.lock and resolves dependencies from the gemspec at image
build time. `mcp "~> 0.14"` therefore floated up to 0.25.0 on a rebuild and
started 403ing every authenticated /mcp call, with no code change.

Both reported symptoms share this cause. resolve_mcp_context halts with 401
before the transport is constructed, so unauthenticated discovery still
worked and the OAuth flow completed; the connector's first authenticated
initialize call then hit the 403 and surfaced it as "Failed to complete
OAuth authorization".

Allow-list the BASE_URL host, and pin mcp to a single minor — since the
gemspec constraints act as production pins here, a floor alone would leave
the next mcp minor free to break this the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ivarsb
ivarsb requested a review from ebeigarts August 7, 2026 07:59
@ebeigarts
ebeigarts merged commit 4a4452b into main Aug 7, 2026
2 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.

2 participants