network-ai: MCP SSE transport reflects localhost CORS Origin regardless of bind interface
What fixes it
- 5.15.2network-ai, npmGitHub
npm install network-ai@5.15.2
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
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).
Impact
Bearer-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:
- Unauthenticated endpoints under the same server (e.g.
/health,/toolslisting where present) are reflected cross-origin. - Any future or downstream code path that assumes "if
Access-Control-Allow-Originwas set, the Origin is trusted local" now has that assumption broken. - Defense-in-depth against token-leakage or misconfigured deployments that bind
0.0.0.0for LAN convenience.
Preconditions:
- Server bound to a non-loopback address (
0.0.0.0or an external IP) - Victim browser visits attacker-controlled page whose Origin matches the regex
Proof of Concept
Start the SSE server bound to a non-loopback address, then from any host with a page served on http://localhost:1234 (attacker machine) issue:
// attacker page at http://localhost:1234 (attacker's box)
fetch('http://<victim-ip>:<mcp-port>/health', { credentials: 'omit' })
.then(r => r.text()).then(console.log);Response headers include Access-Control-Allow-Origin: http://localhost:1234 and Vary: Origin, so the browser hands the body to attacker JS.
Fix
Only 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.
const isLoopbackBind =
this._opts.host === '127.0.0.1' ||
this._opts.host === 'localhost' ||
this._opts.host === '::1';
const origin = req.headers['origin'] ?? '';
const isLocalOrigin = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
if (isLoopbackBind && isLocalOrigin) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
}Branch: 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.
Adversarial review
Before 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.
---
<sub>_Discovered by the Sebastion AI GitHub App._</sub>
Text from GitHub Advisory Database
Affected versions 1
- network-ai, npm<= 5.15.1GitHub