Skip to content

fix(cli): bind MCP HTTP transports to loopback by default - #1697

Merged
phernandez merged 3 commits into
mainfrom
fix/1578-mcp-loopback-default
Oct 9, 2026
Merged

phernandez merged 3 commits into
mainfrom
fix/1578-mcp-loopback-default

Conversation

@phernandez

@phernandez phernandez commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Fixes #1578.

Why

basic-memory mcp --transport streamable-http (and sse) defaulted --host to 0.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

--host now defaults to 127.0.0.1. Binding to 0.0.0.0 or another non-loopback address still works, but the command prints a warning to stderr and logs it. Loopback is decided with ipaddress. localhost counts 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 passes host_origin_protection="auto", and FastMCP builds the guard with its configured allowlists (FASTMCP_HTTP_ALLOWED_HOSTS / FASTMCP_HTTP_ALLOWED_ORIGINS). FastMCP 4.0.3's create_sse_app ignores that option, so for SSE the command installs HostOriginGuardMiddleware itself, built from the same fastmcp.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.0 explicitly, so they keep working.

SECURITY.md now states the loopback default and warns that a non-loopback --host exposes 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_host classifies loopback and non-loopback hosts correctly.
    • Both HTTP transports default to 127.0.0.1.
    • --host 0.0.0.0 prints the warning.
    • streamable-http passes host_origin_protection="auto" with no extra middleware, and SSE passes the guard middleware.
    • The real ASGI path works for both transports. Each test builds the transport's app and sends requests through httpx ASGITransport on a loopback scope:
      • A foreign Host gets 421 and a foreign Origin gets 403.
      • A loopback request reaches the router.
      • Configured http_allowed_hosts / http_allowed_origins entries pass.
      • The guard is installed exactly once.
  • The CLI import stays light: test_cli_exit.py still passes because the fastmcp import is deferred.
  • just man-regen: no changes.
  • Manual runs against a temporary HOME, for both --transport streamable-http and --transport sse (/mcp): the normal Host returned 200, Host: evil.example.com returned 421, and Origin: http://evil.example.com returned 403.

🤖 Generated with Claude Code

https://claude.ai/code/session_01APFUk2bjEwMptMQqpRhjea

`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>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T02:42:39.313780Z 7029530 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

Copy link
Copy Markdown

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: 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".

Comment thread src/basic_memory/cli/commands/mcp.py Outdated
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>

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

Copy link
Copy Markdown

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: 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".

Comment thread src/basic_memory/cli/commands/mcp.py Outdated
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>
@phernandez
phernandez merged commit 7c328a8 into main Oct 9, 2026
35 checks passed
@phernandez
phernandez deleted the fix/1578-mcp-loopback-default branch October 9, 2026 03:19
phernandez added a commit that referenced this pull request Oct 9, 2026
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>
phernandez added a commit that referenced this pull request Oct 9, 2026
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>
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.

Harden HTTP/SSE transport: basic-memory mcp defaults to 0.0.0.0 with no auth (contradicts SECURITY.md)

1 participant