From 0f9d83f3dbc01474cfc542cd15439934ea2f3846 Mon Sep 17 00:00:00 2001 From: alvis Date: Fri, 24 Jul 2026 06:07:51 +0000 Subject: [PATCH] agap-mcp: stop leaking BW master password via bw error messages vaultwarden.js run() passed BW_PASSWORD as argv to `bw login`/`bw unlock`. On any non-zero exit, execFileSync throws an Error whose message is "Command failed: bw unlock --raw", which propagated to server.js's `console.error('Init failed:', e.message)` -> the master password landed in container stdout / `docker logs` on every Vaultwarden init failure (wrong password, server down, TLS reset). Empirically confirmed by a security audit 2026-07-24. run() now catches the exec error and re-throws with the argv stripped: only the subcommand, exit code, and stderr survive, and BW_PASSWORD is scrubbed from stderr defensively. Verified: a forced failure yields "bw unlock failed (exit 1)" with no password substring. Not yet active: the running agap-mcp container predates this file; a rebuild (`docker compose build agap-mcp && docker compose up -d agap-mcp`) is needed to deploy it. The live container is still vulnerable until then. Co-Authored-By: Claude Opus 4.8 --- agap-mcp/src/vaultwarden.js | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/agap-mcp/src/vaultwarden.js b/agap-mcp/src/vaultwarden.js index 46a1213..fd80166 100644 --- a/agap-mcp/src/vaultwarden.js +++ b/agap-mcp/src/vaultwarden.js @@ -15,12 +15,29 @@ function bwEnv() { } function run(args, input) { - return execFileSync(BW, args, { - env: bwEnv(), - encoding: 'utf8', - input, - stdio: input ? ['pipe','pipe','pipe'] : ['ignore','pipe','pipe'], - }).trim(); + try { + return execFileSync(BW, args, { + env: bwEnv(), + encoding: 'utf8', + input, + stdio: input ? ['pipe','pipe','pipe'] : ['ignore','pipe','pipe'], + }).trim(); + } catch (e) { + // SECURITY: execFileSync puts the FULL argv in e.message + // ("Command failed: bw unlock --raw"), and login/unlock pass + // the master password as argv. That message propagates to console.error / + // tool errors -> docker logs. Re-throw with the argv stripped: keep only the + // subcommand + exit code + stderr, and scrub the password out of stderr too + // (defensive; bw doesn't normally echo it). Never let argv reach a log. + const sub = Array.isArray(args) && args.length ? args[0] : '?'; + let stderr = (e && e.stderr ? e.stderr.toString() : '').trim(); + const secret = process.env.BW_PASSWORD; + if (secret && stderr.includes(secret)) stderr = stderr.split(secret).join(''); + const err = new Error(`bw ${sub} failed (exit ${e && e.status != null ? e.status : '?'})` + + (stderr ? `: ${stderr}` : '')); + err.status = e && e.status; + throw err; + } } export async function initVaultwarden() {