GHSA-R78R-RWRF-RJWP

Vulnerability from github – Published: 2026-06-19 13:34 – Updated: 2026-06-19 13:34
VLAI
Summary
Network-AI: CVE-2026-46701 fix incomplete — empty default secret still authorizes all requests
Details

Advisory / Disclosure

Network-AI — CVE-2026-46701 fix is incomplete: the "Empty Default Secret" unauth path survives

Target: Jovancoding/Network-AI (npm network-ai), latest v5.7.1 Status: the advisory ("Unauthenticated Cross-Origin MCP Tool Invocation via Empty Default Secret") named three flaws. The fix (5.4.5) closed the CORS flaw (Access-Control-Allow-Origin is now set only for localhost origins), but left the empty-default-secret flaw the title is about: the SSE MCP server still defaults to an empty secret, _isAuthorized() still returns true when the secret is empty, and a non-loopback bind only warns. So the server still runs fully unauthenticated by default — any non-browser caller (curl, SSRF, or a 0.0.0.0 bind) can invoke all 22 MCP tools (config_set, agent_spawn, blackboard_write, token_*) with no credentials. Class: CWE-306/CWE-862 Missing Authentication — incomplete fix. Methodology: M1 incomplete-fix audit (anchor = the 5.4.5 fix; sibling-walk on latest v5.7.1, executed). Severity: High (matches parent; the browser amplifier is removed, so exploitation now needs non-browser reach — SSRF or a non-loopback bind, which the fix only warns about).

What the fix did and didn't do (verified on latest v5.7.1)

advisory flaw latest v5.7.1
wildcard CORS (ACAO: *) FIXEDlib/mcp-transport-sse.ts sets ACAO only when origin matches ^https?://(localhost\|127\.0\.0\.1)(:\d+)?$
empty default secret NOT FIXEDbin/mcp-server.ts: secret: process.env['NETWORK_AI_MCP_SECRET'] ?? ''
_isAuthorized open on empty secret NOT FIXEDif (!this._opts.secret) return true;
require secret / refuse unauth bind NOT DONElisten() only process.stderr.write('… WARNING …') on non-loopback bind, then listens anyway

The advisory's remediation #1 ("Require a non-empty secret at startup … process.exit(1)") was not implemented.

PoC (executed against the latest source, v5.7.1) — poc/legend-networkai-empty-secret.ts

Instantiates the real McpSseServer from the latest lib/ with a mock bridge and the default (empty) secret, then issues requests (run-log poc/run-log.txt):

POST /mcp  no-auth, no-origin (curl/SSRF) -> HTTP 200, dispatched=true
   body: {"jsonrpc":"2.0","id":1,"result":{"executed":true,"tool":"config_set"}}
POST /mcp  Origin: evil.example.com        -> ACAO=undefined   (CORS half fixed)

The no-auth request passes _isAuthorized and reaches handleRPC (tool dispatched) — i.e. unauthenticated tool invocation persists on the latest release; only the browser-CORS read amplifier was removed.

Run: from a v5.7.1 checkout, npm i then npx ts-node --transpile-only poc/legend-networkai-empty-secret.ts.

Recommended fix

Implement the advisory's remediation #1: refuse to start SSE mode with an empty secret (unless --stdio), and/or change _isAuthorized to fail closed (an empty configured secret should mean "deny", not "allow"). The CORS allowlist alone does not authenticate non-browser callers.

Precondition / honesty

With CORS now localhost-only, the drive-by browser attack is mitigated. The residual requires a non-browser path to the port: an SSRF on the host, or the operator binding to a non-loopback address (Docker/remote), which the fix only warns about. The empty secret remains the shipped default and _isAuthorized still authorizes it.

Credits

@Kai Aizen / @SnailSploit — https://snailsploit.com

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 5.7.1"
      },
      "package": {
        "ecosystem": "npm",
        "name": "network-ai"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "5.7.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-48814"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-306"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-19T13:34:24Z",
    "nvd_published_at": "2026-06-17T20:17:22Z",
    "severity": "CRITICAL"
  },
  "details": "## Advisory / Disclosure\n\n# Network-AI \u2014 CVE-2026-46701 fix is incomplete: the \"Empty Default Secret\" unauth path survives\n\n**Target:** Jovancoding/Network-AI (npm `network-ai`), **latest v5.7.1**\n**Status:** the advisory (\"Unauthenticated Cross-Origin MCP Tool Invocation via Empty\nDefault Secret\") named three flaws. The fix (5.4.5) closed the **CORS** flaw\n(`Access-Control-Allow-Origin` is now set only for localhost origins), but left the\n**empty-default-secret** flaw the title is about: the SSE MCP server still defaults to an\nempty secret, `_isAuthorized()` still returns `true` when the secret is empty, and a\nnon-loopback bind only **warns**. So the server still runs **fully unauthenticated by\ndefault** \u2014 any non-browser caller (curl, SSRF, or a `0.0.0.0` bind) can invoke all 22 MCP\ntools (`config_set`, `agent_spawn`, `blackboard_write`, `token_*`) with no credentials.\n**Class:** CWE-306/CWE-862 Missing Authentication \u2014 incomplete fix.\n**Methodology:** M1 incomplete-fix audit (anchor = the 5.4.5 fix; sibling-walk on latest v5.7.1, executed).\n**Severity:** High (matches parent; the browser amplifier is removed, so exploitation now\nneeds non-browser reach \u2014 SSRF or a non-loopback bind, which the fix only warns about).\n\n## What the fix did and didn\u0027t do (verified on latest v5.7.1)\n| advisory flaw | latest v5.7.1 |\n|---|---|\n| wildcard CORS (`ACAO: *`) | **FIXED** \u2014 `lib/mcp-transport-sse.ts` sets `ACAO` only when `origin` matches `^https?://(localhost\\|127\\.0\\.0\\.1)(:\\d+)?$` |\n| empty default secret | **NOT FIXED** \u2014 `bin/mcp-server.ts`: `secret: process.env[\u0027NETWORK_AI_MCP_SECRET\u0027] ?? \u0027\u0027` |\n| `_isAuthorized` open on empty secret | **NOT FIXED** \u2014 `if (!this._opts.secret) return true;` |\n| require secret / refuse unauth bind | **NOT DONE** \u2014 `listen()` only `process.stderr.write(\u0027\u2026 WARNING \u2026\u0027)` on non-loopback bind, then listens anyway |\n\nThe advisory\u0027s remediation #1 (\"Require a non-empty secret at startup \u2026 `process.exit(1)`\")\nwas not implemented.\n\n## PoC (executed against the latest source, v5.7.1) \u2014 `poc/legend-networkai-empty-secret.ts`\nInstantiates the real `McpSseServer` from the latest `lib/` with a mock bridge and the\n**default (empty) secret**, then issues requests (run-log `poc/run-log.txt`):\n\n```\nPOST /mcp  no-auth, no-origin (curl/SSRF) -\u003e HTTP 200, dispatched=true\n   body: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"executed\":true,\"tool\":\"config_set\"}}\nPOST /mcp  Origin: evil.example.com        -\u003e ACAO=undefined   (CORS half fixed)\n```\nThe no-auth request passes `_isAuthorized` and reaches `handleRPC` (tool dispatched) \u2014 i.e.\nunauthenticated tool invocation persists on the latest release; only the browser-CORS read\namplifier was removed.\n\nRun: from a v5.7.1 checkout, `npm i` then\n`npx ts-node --transpile-only poc/legend-networkai-empty-secret.ts`.\n\n## Recommended fix\nImplement the advisory\u0027s remediation #1: refuse to start SSE mode with an empty secret\n(unless `--stdio`), and/or change `_isAuthorized` to fail closed (an empty configured\nsecret should mean \"deny\", not \"allow\"). The CORS allowlist alone does not authenticate\nnon-browser callers.\n\n## Precondition / honesty\nWith CORS now localhost-only, the drive-by *browser* attack is mitigated. The residual\nrequires a non-browser path to the port: an SSRF on the host, or the operator binding to a\nnon-loopback address (Docker/remote), which the fix only warns about. The empty secret\nremains the shipped default and `_isAuthorized` still authorizes it.\n\n## Credits\n\n@Kai Aizen / @SnailSploit \u2014 https://snailsploit.com",
  "id": "GHSA-r78r-rwrf-rjwp",
  "modified": "2026-06-19T13:34:24Z",
  "published": "2026-06-19T13:34:24Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Jovancoding/Network-AI/security/advisories/GHSA-r78r-rwrf-rjwp"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-48814"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Jovancoding/Network-AI"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Jovancoding/Network-AI/releases/tag/v5.7.2"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-j3vx-cx2r-pvg8"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Network-AI: CVE-2026-46701 fix incomplete \u2014 empty default secret still authorizes all requests"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…