Repository navigation
fix(cli): bind MCP HTTP transports to loopback by default - #1697
Conversation
`basic-memory mcp --transport streamable-http|sse` defaulted --host to 0.0.0.0 with no inbound authentication, contradicting SECURITY.md. - Default --host to 127.0.0.1; 0.0.0.0 stays an explicit opt-in - Warn on stderr and in the MCP log when the bind host is not loopback - Pass host_origin_protection="auto" so FastMCP rejects foreign Host/Origin headers on loopback (DNS rebinding) - Document the defaults in SECURITY.md Docker and docker-compose already pass --host 0.0.0.0 explicitly. Fixes #1578 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APFUk2bjEwMptMQqpRhjea Signed-off-by: phernandez <paul@basicmachines.co>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9642274715
ℹ️ 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".
FastMCP 4.0.3 applies host_origin_protection only to the streamable-http app; create_sse_app ignores it, so `--transport sse` had no DNS-rebinding protection. Pass HostOriginGuardMiddleware(mode="auto") as middleware for both transports instead, leaving host_origin_protection off so streamable-http is not guarded twice. Tests build each transport's app and send foreign Host/Origin requests through httpx's ASGITransport over a loopback scope (421/403), and check a loopback request still reaches the router. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APFUk2bjEwMptMQqpRhjea Signed-off-by: phernandez <paul@basicmachines.co>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: acaf7538c7
ℹ️ 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".
Our own HostOriginGuardMiddleware ignored FASTMCP_HTTP_ALLOWED_HOSTS and FASTMCP_HTTP_ALLOWED_ORIGINS, so a loopback deployment behind a local reverse proxy got 421/403 on streamable-http. - streamable-http: pass host_origin_protection="auto" and let FastMCP build the guard with its configured allowlists - sse: FastMCP installs no guard there, so keep our middleware but build it from fastmcp.settings.http_allowed_hosts / http_allowed_origins Tests drive both transports through the real ASGI app: foreign Host and Origin are rejected, configured allowlist entries pass, and the guard is installed once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APFUk2bjEwMptMQqpRhjea Signed-off-by: phernandez <paul@basicmachines.co>
Entries for #1697, #1698, #1699, #1700, #1701, #1702, #1704 and #1705, which landed without changelog edits so the parallel PRs would not conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APFUk2bjEwMptMQqpRhjea Signed-off-by: phernandez <paul@basicmachines.co>
Entries for #1697, #1698, #1699, #1700, #1701, #1702, #1704 and #1705, which landed without changelog edits so the parallel PRs would not conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APFUk2bjEwMptMQqpRhjea Signed-off-by: phernandez <paul@basicmachines.co>
Fixes #1578.
Why
basic-memory mcp --transport streamable-http(andsse) defaulted--hostto0.0.0.0. These transports have no inbound authentication, so choosing an HTTP transport exposed every MCP tool to the LAN. That contradicts SECURITY.md, which says Basic Memory opens no ports by default and should be bound to localhost. A loopback listener with no Host/Origin check can also be reached through DNS rebinding from a browser tab.Change
--hostnow defaults to127.0.0.1. Binding to0.0.0.0or another non-loopback address still works, but the command prints a warning to stderr and logs it. Loopback is decided withipaddress.localhostcounts as loopback, and any other hostname counts as exposed.Both HTTP transports now get FastMCP's Host/Origin guard in "auto" mode, through
http_guard_options(transport). For streamable-http the command passeshost_origin_protection="auto", and FastMCP builds the guard with its configured allowlists (FASTMCP_HTTP_ALLOWED_HOSTS/FASTMCP_HTTP_ALLOWED_ORIGINS). FastMCP 4.0.3'screate_sse_appignores that option, so for SSE the command installsHostOriginGuardMiddlewareitself, built from the samefastmcp.settings.http_allowed_hosts/http_allowed_origins. Each transport gets the guard exactly once. In auto mode, requests that arrive over loopback need a loopback Host header (421 otherwise) and a same-origin or loopback Origin (403 otherwise). Requests that arrive on non-loopback interfaces are not Host-checked. The Docker image and docker-compose setup already pass--host 0.0.0.0explicitly, so they keep working.SECURITY.md now states the loopback default and warns that a non-loopback
--hostexposes an unauthenticated server. The CHANGELOG entry is left for the maintainer.Verification
just fast-check: lint, format and ty typecheck pass.BASIC_MEMORY_TESTMON_SELECT_FLAGS="--import-mode=importlib" just fast-test tests/cli/test_mcp_command.py test-int/cli/test_routing_integration.py tests/cli/test_cli_exit.py: 58 passed. The new tests check the following:is_loopback_hostclassifies loopback and non-loopback hosts correctly.--host 0.0.0.0prints the warning.host_origin_protection="auto"with no extra middleware, and SSE passes the guard middleware.ASGITransporton a loopback scope:http_allowed_hosts/http_allowed_originsentries pass.test_cli_exit.pystill passes because the fastmcp import is deferred.just man-regen: no changes.--transport streamable-httpand--transport sse(/mcp): the normal Host returned 200,Host: evil.example.comreturned 421, andOrigin: http://evil.example.comreturned 403.🤖 Generated with Claude Code
https://claude.ai/code/session_01APFUk2bjEwMptMQqpRhjea