feat(web): enforce localhost-only binding; expand loopback origin allow-list (#1796)

Closes #1795.

Enforce localhost-only binding in code (the "never 0.0.0.0" rule was only a spec note): refuse an all-interfaces HOST — across every spelling the OS binds as the wildcard (inet_aton decimal/octal/hex, IPv6, IPv4-mapped, zone-scoped, IDNA/Unicode) — unless DANGEROUSLY_BIND_ALL_INTERFACES=true (Docker's sanctioned opt-in). Fixes the reported stdio-connect 403 by expanding the default loopback origin allow-list to all three interchangeable forms.

Hardening surfaced across an extended review: closes an IDNA/fullwidth wildcard bypass, a blank-ALLOWED_ORIGINS fail-open, and the CLI/TUI OAuth-callback wildcard bind (loopback-only now, RFC 8252); canonicalizes the origin allow-list the way a browser does; derives frame-ancestors from the real embedder list (IPv6-literal exclusion verified in headless Chromium); strictPort on all three bind points; and preserves the CLI exit-code map + JSON envelope through the mcp-inspector --cli launcher path. Shared host normalization lives in core/node/hostUrl.ts. Docs + tests + smokes throughout.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Cliff Hall 2026-07-26 22:15:10 -04:00 • committed by GitHub
commit cea22d86b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -168,8 +168,13 @@ ssh -L 6274:127.0.0.1:6274 -L 6275:127.0.0.1:6275 <remote-host>
Then open `http://127.0.0.1:6274/?MCP_INSPECTOR_API_TOKEN=$TOKEN` locally.
- Use `127.0.0.1`, **not** `localhost` — the backend's origin guard checks the
literal `Origin` header against the configured `HOST`.
- Use `127.0.0.1` or `localhost` — the default origin allow-list emits both (and
`[::1]`) for a loopback bind (#1795), so the guard accepts either, whichever
the browser sends. **Do not** use a bare `http://[::1]:…` URL for App review:
it connects, but a bracketed IPv6 literal isn't a valid CSP `frame-ancestors`
source, so the App sandbox iframe is CSP-blocked and the widget never renders
(see the MCP Apps caveat in [`clients/web/README.md`](../clients/web/README.md#host-binding--the-origin-allow-list)).
A **non-loopback** forward target needs `ALLOWED_ORIGINS` set to that origin.
- Forward `:6275` (`MCP_SANDBOX_PORT`) as well: the Apps tab renders widgets in
an iframe served from that second origin; without it the App frame stays
blank.