{"schema":"threatline.record/2","record":{"id":"GHSA-4pvg-m42h-c3x2","aliases":["GHSA-4pvg-m42h-c3x2"],"summary":"MCP SSE transport reflects localhost CORS Origin regardless of bind interface","details":"## Summary\n\n`McpSseServer._handleRequest` (`lib/mcp-transport-sse.ts`) reflects any `Origin` matching `^https?://(localhost|127\\.0\\.0\\.1)(:\\d+)?$` into `Access-Control-Allow-Origin` regardless of the interface the server is bound to. When the server is started with `--host 0.0.0.0` (or any non-loopback bind) and reachable to a network peer, a `localhost` Origin no longer implies same-host, and a browser page served from the attacker's own `http://localhost:<port>` (or a DNS-rebinding target that briefly resolves to a `localhost`-looking name) will pass the check and receive the reflection. This is CWE-346 (Origin Validation Error) / CWE-1385 (Missing Origin Validation in WebSockets-adjacent transports).\n\n## Impact\n\nBearer-token auth still gates `/mcp` and `/sse` handlers, so the attacker page cannot invoke tool calls without also holding the token. The practical exposure is:\n\n- Unauthenticated endpoints under the same server (e.g. `/health`, `/tools` listing where present) are reflected cross-origin.\n- Any future or downstream code path that assumes \"if `Access-Control-Allow-Origin` was set, the Origin is trusted local\" now has that assumption broken.\n- Defense-in-depth against token-leakage or misconfigured deployments that bind `0.0.0.0` for LAN convenience.\n\nPreconditions:\n- Server bound to a non-loopback address (`0.0.0.0` or an external IP)\n- Victim browser visits attacker-controlled page whose Origin matches the regex\n\n## Proof of Concept\n\nStart the SSE server bound to a non-loopback address, then from any host with a page served on `http://localhost:1234` (attacker machine) issue:\n\n```js\n// attacker page at http://localhost:1234 (attacker's box)\nfetch('http://<victim-ip>:<mcp-port>/health', { credentials: 'omit' })\n  .then(r => r.text()).then(console.log);\n```\n\nResponse headers include `Access-Control-Allow-Origin: http://localhost:1234` and `Vary: Origin`, so the browser hands the body to attacker JS.\n\n## Fix\n\nOnly reflect the localhost Origin when the server itself is bound to a loopback interface. In every other bind mode, do not set `Access-Control-Allow-Origin` — browser cross-origin callers are not a supported deployment shape for MCP transport.\n\n```ts\nconst isLoopbackBind =\n  this._opts.host === '127.0.0.1' ||\n  this._opts.host === 'localhost' ||\n  this._opts.host === '::1';\nconst origin = req.headers['origin'] ?? '';\nconst isLocalOrigin = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1)(:\\d+)?$/.test(origin);\nif (isLoopbackBind && isLocalOrigin) {\n  res.setHeader('Access-Control-Allow-Origin', origin);\n  res.setHeader('Vary', 'Origin');\n}\n```\n\nBranch: `fix/cwe918-mcp-transport-sse-cors-ee44` on fork `sebastiondev/Network-AI` — single commit, +16/-2 in `lib/mcp-transport-sse.ts`. `npx tsc --noEmit` clean.\n\n## Adversarial review\n\nBefore submitting we tried to disprove the finding. Bearer auth on `/mcp` and `/sse` does bound most impact, and `listen()` fails closed without a secret, so the token-authenticated tool surface is not directly reachable from a browser page that lacks the token. What remains is (a) any unauthenticated auxiliary endpoint on the same origin, and (b) the assumption in downstream code that \"ACAO reflected == trusted local caller\". Both are worth closing at the transport layer; the fix is 16 lines and preserves behaviour for the default loopback deployment.\n\n---\n\n<sub>_Discovered by the [Sebastion AI GitHub App](https://github.com/apps/sebastionai)._</sub>\n","categories":["Unclassified"],"product":"network-ai","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/Jovancoding/Network-AI/security/advisories/GHSA-4pvg-m42h-c3x2","source_modified_at":"2026-09-27T20:41:41Z","fetched_at":"2026-10-23T18:48:36.087640Z","package":{"ecosystem":"npm","name":"network-ai"},"range":"<= 5.15.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/Jovancoding/Network-AI/security/advisories/GHSA-4pvg-m42h-c3x2","source_modified_at":"2026-09-27T20:41:41Z","fetched_at":"2026-10-23T18:48:36.087640Z","package":{"ecosystem":"npm","name":"network-ai"},"version":"5.15.2"}],"cvss":null,"severity":"Low","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-27T20:41:41Z","modified_at":"2026-09-27T20:41:41Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/Jovancoding/Network-AI/security/advisories/GHSA-4pvg-m42h-c3x2","fetched_at":"2026-10-23T18:48:36.087640Z","source_modified_at":"2026-09-27T20:41:41Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-09-27T20:41:41Z","content_hash":"393a9b272dca2deabb6e2c2d99e1398fce6c5caa2999653ad78041ba47c55e60"}}
