GHSA-mxjx-28vx-xjjj Network-AI: ApprovalInbox HTTP server has no authentication — anyone can approve pending agent actions
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**.DetailsNo 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**:`routeRequest()` exposes list + approve/deny with no credential check (default `pathPrefix` = `/approvals`):`approve()` resolves the pending promise that `ApprovalGate` is awaiting, so the gated action proceeds:`startServer()` binds the handler (default `127.0.0.1`, but any host the caller passes):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 mattersThe 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 ConceptSelf-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.`poc.mjs`:Output: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):ImpactThe 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**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`.**Remove the hardcoded `Access-Control-Allow-Origin: *`**; default to no CORS (same-origin) or an explicit allowlist, and never reflect `*` on the mutating routes.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.
CVSS v3.1 score: 5.9