Skip to content

fix(security): bind the HTTP/SSE surface to loopback and authenticate remote access#55

Merged
aaronjmars merged 1 commit into
mainfrom
security/http-loopback-and-auth
Jul 20, 2026
Merged

fix(security): bind the HTTP/SSE surface to loopback and authenticate remote access#55
aaronjmars merged 1 commit into
mainfrom
security/http-loopback-and-auth

Conversation

@aaronjmars

Copy link
Copy Markdown
Collaborator

Closes #54.

POST /sse hands its body straight to handleMCPRequest, which drives the
browser through the extension, and it was reachable two ways it should not have
been: the Express app listened on all interfaces, and CORS answered every
preflight with Access-Control-Allow-Origin: *. So any LAN peer could call it,
and any website the user visited could call it cross-origin and read the reply.
#53 closed the equivalent hole on the WebSocket channel; this is the other half.

Three changes, mirroring what #53 established:

  • Bind the HTTP server to 127.0.0.1. ngrok http runs on this machine and
    forwards to localhost, and the extension fetches /ports from localhost, so
    nothing legitimate needed the wider bind. --http-host= widens it deliberately.
  • Gate every request on Origin. Browsers send Origin cross-origin, so page
    requests (http(s), and "null" from a sandbox) are refused 403 while extension
    service workers and non-browser MCP clients pass. CORS now reflects only
    allowed origins instead of *, so a page cannot read a response either.
  • Require a bearer token once the surface leaves this machine — --tunnel or a
    non-loopback --http-host. Generated and printed at startup, or pinned with
    --token=. The tunnel was previously unauthenticated to anyone who had the URL.

Verified against a running server. Default (local) mode:

bind address 127.0.0.1:5556 (was *:5556)
LAN POST /sse ECONNREFUSED
loopback, no Origin 200
loopback, Origin https://evil 403
loopback, simple req, no preflight 403
loopback, Origin null 403
GET /ports, chrome-extension:// 200
preflight from evil page 403, no ACAO header
preflight from extension 204, ACAO reflects the extension

With --http-host=0.0.0.0:

LAN POST /sse, no token 401
LAN POST /sse, wrong token 401
LAN POST /sse, token prefix 401
LAN POST /sse, correct token 200
LAN POST /sse, token + evil Origin 403

The WebSocket channel stays on loopback regardless of --http-host.

Co-Authored-By: Claude noreply@anthropic.com

… remote access

Closes #54.

`POST /sse` hands its body straight to handleMCPRequest, which drives the
browser through the extension, and it was reachable two ways it should not have
been: the Express app listened on all interfaces, and CORS answered every
preflight with `Access-Control-Allow-Origin: *`. So any LAN peer could call it,
and any website the user visited could call it cross-origin and read the reply.
#53 closed the equivalent hole on the WebSocket channel; this is the other half.

Three changes, mirroring what #53 established:

- Bind the HTTP server to 127.0.0.1. `ngrok http` runs on this machine and
  forwards to localhost, and the extension fetches /ports from localhost, so
  nothing legitimate needed the wider bind. --http-host= widens it deliberately.
- Gate every request on Origin. Browsers send Origin cross-origin, so page
  requests (http(s), and "null" from a sandbox) are refused 403 while extension
  service workers and non-browser MCP clients pass. CORS now reflects only
  allowed origins instead of `*`, so a page cannot read a response either.
- Require a bearer token once the surface leaves this machine — --tunnel or a
  non-loopback --http-host. Generated and printed at startup, or pinned with
  --token=. The tunnel was previously unauthenticated to anyone who had the URL.

Verified against a running server. Default (local) mode:

  bind address                        127.0.0.1:5556 (was *:5556)
  LAN POST /sse                       ECONNREFUSED
  loopback, no Origin                 200
  loopback, Origin https://evil       403
  loopback, simple req, no preflight  403
  loopback, Origin null               403
  GET /ports, chrome-extension://     200
  preflight from evil page            403, no ACAO header
  preflight from extension            204, ACAO reflects the extension

With --http-host=0.0.0.0:

  LAN POST /sse, no token             401
  LAN POST /sse, wrong token          401
  LAN POST /sse, token prefix         401
  LAN POST /sse, correct token        200
  LAN POST /sse, token + evil Origin  403

The WebSocket channel stays on loopback regardless of --http-host.

Co-Authored-By: Claude <noreply@anthropic.com>
@aaronjmars
aaronjmars merged commit ea71bb9 into main Jul 20, 2026
2 checks passed
@aaronjmars
aaronjmars deleted the security/http-loopback-and-auth branch July 20, 2026 16:58
aaronjmars added a commit that referenced this pull request Jul 20, 2026
…changes (#57)

* docs: add a changelog and a release checklist that surfaces breaking changes

#55 made `--tunnel` require a bearer token on /sse. That is a real break for
anyone running a tunnelled connector today — it will present as "my ChatGPT
connector stopped working" the moment they upgrade — and right now nothing in the
repo would remind us to say so at tag time. Release notes are hand-written per
release, and `.github/workflows/publish.yml` pushes to npm on a `v*` tag, so the
tag is the point of no return.

Add `.github/CHANGELOG.md`, seeded with an Unreleased section covering everything
landed since v1.1.1 (13 commits): the tunnel token under ⚠️ Breaking with the
migration line spelled out, the three security fixes (#53, #55, #56), the
node-forge/brace-expansion bump (#43), and the dependency and docs maintenance.

Add a Releasing section to CONTRIBUTING that makes reading Unreleased step 1 of
cutting a tag, and states that a ⚠️ Breaking entry means a major bump with the
migration step at the top of the release notes. Also ask PR authors to add their
own entry, so the file stays accurate instead of being reconstructed later.

No code changes.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: link the adm-zip override to its removal issue (#58)

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

Unauthenticated MCP control via POST /sse — reachable from LAN and from any website

1 participant