Skip to content

Relax host origin guard defaults - #4439

Merged
jlowin merged 2 commits into
release/3.xfrom
codex/host-origin-auto-release-3x
Jul 7, 2026
Merged

jlowin merged 2 commits into
release/3.xfrom
codex/host-origin-auto-release-3x

Conversation

@jlowin

@jlowin jlowin commented Jul 6, 2026

Copy link
Copy Markdown
Member

The 3.4.3 Host/Origin guard fixed the localhost DNS rebinding path, but it also made ASGI, serverless, and reverse-proxy deployments inherit strict Host rejection even when FastMCP cannot reliably infer the external hostname. That turned a browser-boundary security fix into a deployment compatibility break for environments like Lambda/API Gateway.

This changes the default to auto: localhost-bound direct servers stay protected, explicit allowed_hosts and host_origin_protection=True keep strict validation available, and ambiguous ASGI/serverless scopes no longer get surprise 421s for their public Host header.

from fastmcp import FastMCP

mcp = FastMCP("My Server")

# Serverless/proxy apps keep accepting their public Host by default.
app = mcp.http_app()

# Deployments that know their public hostname can opt into strict Host trust.
strict_app = mcp.http_app(allowed_hosts=["mcp.example.com"])

@marvin-context-protocol marvin-context-protocol Bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. http Related to HTTP transport, networking, or web server functionality. high-priority labels Jul 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

馃挕 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 53ca4d9cb2

鈩癸笍 About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 馃憤.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

and resolved_allowed_hosts is None
and _is_loopback_host(host)
):
resolved_allowed_hosts = [host]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Preserve configured allowed hosts in direct runs

When run(transport="http") binds to the default loopback host and the user configured FASTMCP_HTTP_ALLOWED_HOSTS / fastmcp.settings.http_allowed_hosts, the allowed_hosts argument is still None here. This assignment makes it non-None before calling http_app, so http_app no longer falls back to the configured setting and only trusts [host]; the configured public host will still get a 421. Please merge the setting with the loopback host instead of replacing it.

Useful? React with 馃憤聽/ 馃憥.

await self.app(scope, receive, send)

def _should_validate_host(self, scope: Scope) -> bool:
if self.mode == "strict" or self.has_explicit_allowed_hosts:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Validate Host when only origins are configured

For http_app(allowed_origins=[...]) in ASGI/serverless scopes where scope["server"] is unset, this leaves Host validation disabled because only has_explicit_allowed_hosts is considered. _origin_allowed then accepts any same-origin Host/Origin pair, so Host: attacker.example with Origin: https://attacker.example reaches the MCP endpoint even though that origin is not in allowed_origins. Please also establish a trusted Host boundary when an origin allowlist is configured, or skip the same-origin fallback without one.

Useful? React with 馃憤聽/ 馃憥.

@jlowin
jlowin merged commit 400db61 into release/3.x Jul 7, 2026
9 checks passed
@jlowin
jlowin deleted the codex/host-origin-auto-release-3x branch July 7, 2026 00:59
jlowin added a commit that referenced this pull request Jul 19, 2026
* Add OAuthProxy issuer response parameter

* Cover OAuthProxy issuer error redirects

* Relax host origin guard defaults (#4439)

* Use exact issuer in authorize errors

* Restore HTTP host guard compatibility (#4472)

* Hugging Face Auth Integration (#4385)

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>

* Docs: add v3.4.4 changelog entries (#4473)

* Explain unnormalized issuer; cover consent-denial path base_url

* Revert "Merge remote-tracking branch 'origin/release/3.x' into codex/oauth-proxy-rfc9207-issuer"

This reverts commit 9e34b16, reversing
changes made to 640dc60.

* Preserve callback query bytes when appending iss/code/state params

add_query_params previously decoded the existing query with parse_qsl
and re-encoded it, mutating opaque or signed query strings (a valueless
?flag became ?flag=, non-UTF-8 percent-encoded bytes got replaced).
Append the newly-encoded params to the existing query string instead of
round-tripping it through parse/encode.

Also fixes a stray bare `httpx` reference in a test that should use
httpx2 following the SDK v2 migration.

* Attach RFC 9207 iss to authorize() success redirects too

AuthorizationHandler only added iss to error redirects from the SDK's
base handler, not to code redirects returned directly by authorize()
overrides that bypass consent/upstream (as GitHub's mocked test does).
Since metadata now unconditionally advertises
authorization_response_iss_parameter_supported, any client-facing
redirect missing iss hard-fails RFC 9207-aware clients.

Also fixes HeadlessOAuth, which parsed code/state from the redirect
but silently dropped iss, so the same regression would have masked
itself across every other provider integration test too.

* Carry RFC 9207 iss through the production OAuth callback path

OAuthProxy advertises authorization_response_iss_parameter_supported and
sends iss on every authorization redirect, but the client's production
callback chain (CallbackResponse -> OAuthCallbackResult -> OAuth.callback_handler)
had no iss field, so it was silently dropped and the SDK's
validate_authorization_response_iss rejected the callback. HeadlessOAuth
already carried iss through, which is why CI stayed green while real
clients failed.

Add iss to CallbackResponse and OAuthCallbackResult, thread it through
store_result_once for both success and error branches, and pass it into
AuthorizationCodeResult in OAuth.callback_handler.

* Don't duplicate iss when a provider redirect already carries one

* Consolidate RFC 9207 iss handling into a single redirect helper

Every client-facing authorization redirect must carry exactly one iss.
That invariant was being enforced by hand at five separate call sites,
each building its own params dict -- which is how the success-redirect
path shipped without iss in the first place, and how a registered
redirect_uri that already carries its own iss could end up duplicated.
Route all five sites through build_client_redirect(), which owns the
idempotent replace-or-append behavior so no caller can get it wrong.

---------

Co-authored-by: shaun smith <1936278+evalstate@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. http Related to HTTP transport, networking, or web server functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant