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 <BW_PASSWORD> --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 <noreply@anthropic.com>
120 lines
3.8 KiB
JavaScript
120 lines
3.8 KiB
JavaScript
import { execFileSync } from 'child_process';
|
|
|
|
const BW = 'bw';
|
|
const ORG_ID = '4bd75130-b4d3-48d4-a4cb-e52b70295a51';
|
|
const AI_COLLECTION = '5be27a82-8475-4c38-96b2-fa94ec8c957b';
|
|
|
|
let _session = null;
|
|
|
|
function bwEnv() {
|
|
const env = { ...process.env };
|
|
for (const k of ['HTTPS_PROXY','HTTP_PROXY','ALL_PROXY','https_proxy','http_proxy','all_proxy'])
|
|
delete env[k];
|
|
env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
return env;
|
|
}
|
|
|
|
function run(args, input) {
|
|
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 <BW_PASSWORD> --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('<redacted>');
|
|
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() {
|
|
const email = process.env.BW_EMAIL || 'allogn@gmail.com';
|
|
const password = process.env.BW_PASSWORD;
|
|
|
|
// Data dir is mounted from host — server already configured, skip bw config server
|
|
|
|
let status = 'unauthenticated';
|
|
try {
|
|
status = JSON.parse(run(['status'])).status;
|
|
} catch {}
|
|
|
|
if (status === 'unauthenticated') {
|
|
run(['login', email, password, '--raw']);
|
|
}
|
|
|
|
_session = run(['unlock', password, '--raw']);
|
|
run(['sync', '--session', _session]);
|
|
console.log('Vaultwarden: ready');
|
|
}
|
|
|
|
function session() {
|
|
if (!_session) throw new Error('Vaultwarden not initialized');
|
|
return _session;
|
|
}
|
|
|
|
export function vwGetPassword(name) {
|
|
return run(['get', 'password', name, '--session', session()]);
|
|
}
|
|
|
|
export function vwGetItem(name) {
|
|
return JSON.parse(run(['get', 'item', name, '--session', session()]));
|
|
}
|
|
|
|
export function vwListItems(search) {
|
|
const args = ['list', 'items', '--session', session()];
|
|
if (search) args.push('--search', search);
|
|
return JSON.parse(run(args));
|
|
}
|
|
|
|
export function vwListOrgItems(search) {
|
|
const args = ['list', 'items', '--organizationid', ORG_ID, '--session', session()];
|
|
if (search) args.push('--search', search);
|
|
return JSON.parse(run(args));
|
|
}
|
|
|
|
export function vwCreateLogin({ name, username, password, url, notes }) {
|
|
const item = {
|
|
organizationId: ORG_ID,
|
|
collectionIds: [AI_COLLECTION],
|
|
folderId: null,
|
|
type: 1,
|
|
name,
|
|
notes: notes || null,
|
|
favorite: false,
|
|
login: {
|
|
username: username || null,
|
|
password,
|
|
uris: url ? [{ match: null, uri: url }] : [],
|
|
},
|
|
};
|
|
const encoded = run(['encode'], JSON.stringify(item));
|
|
return JSON.parse(run(['create', 'item', encoded, '--session', session()]));
|
|
}
|
|
|
|
export function vwUpdatePassword(nameOrId, newPassword) {
|
|
let item;
|
|
try {
|
|
item = JSON.parse(run(['get', 'item', nameOrId, '--session', session()]));
|
|
} catch {
|
|
const items = vwListOrgItems(nameOrId);
|
|
item = items.find(i => i.name === nameOrId);
|
|
if (!item) throw new Error(`Item not found: ${nameOrId}`);
|
|
}
|
|
item.login.password = newPassword;
|
|
const encoded = run(['encode'], JSON.stringify(item));
|
|
return JSON.parse(run(['edit', 'item', item.id, encoded, '--session', session()]));
|
|
}
|