All vulnerabilities
GHSA-4pvg-m42h-c3x2

network-ai: MCP SSE transport reflects localhost CORS Origin regardless of bind interface

network-ai
CVSS
n/aLow
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in5.15.2Fixed versionGitHub
Published27 Sep 2026Updated 27 Sep 2026GitHub

What fixes it

  • 5.15.2network-ai, npmGitHubnpm 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, /tools listing where present) are reflected cross-origin.
  • 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.
  • Defense-in-depth against token-leakage or misconfigured deployments that bind 0.0.0.0 for LAN convenience.

Preconditions:

  • Server bound to a non-loopback address (0.0.0.0 or 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