mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
fix(security): stop leaking the vault master password via process argv (#879)
The /api/vault/unlock handler ran `bw` as `_run_bw(["unlock", req.master_password, "--raw"])`. _run_bw launches it with `asyncio.create_subprocess_exec(bw_path, *args)`, so the master password became a process argument — readable by any local user through `ps` and `/proc/<pid>/cmdline` for the lifetime of the unlock subprocess. The Bitwarden master password decrypts the entire vault, so this is a serious credential exposure on any multi-user / shared host (CWE-214). The sibling /login handler already avoids this by feeding the password on stdin; unlock was the outlier. Hand the password to `bw` through the environment instead (`--passwordenv BW_PASSWORD`), mirroring how BW_SESSION is already passed — `/proc/<pid>/environ` is readable only by the process owner, not other local users. Add regression tests pinning that the secret reaches the subprocess env and never appears in argv.
This commit is contained in:
+15
-2
@@ -75,11 +75,19 @@ def _save_config(cfg: dict):
|
||||
safe_chmod(str(VAULT_FILE), 0o600)
|
||||
|
||||
|
||||
async def _run_bw(args: list, session: str = None, input_text: str = None) -> tuple:
|
||||
async def _run_bw(args: list, session: str = None, input_text: str = None,
|
||||
bw_password: str = None) -> tuple:
|
||||
env = {}
|
||||
env.update(os.environ)
|
||||
if session:
|
||||
env["BW_SESSION"] = session
|
||||
# Secrets must never be passed as argv — process arguments are world-readable
|
||||
# via `ps` / `/proc/<pid>/cmdline` to any local user. Hand the master password
|
||||
# to `bw` through the environment instead (paired with `--passwordenv
|
||||
# BW_PASSWORD` in args); /proc/<pid>/environ is readable only by the process
|
||||
# owner. This mirrors how BW_SESSION is already passed above.
|
||||
if bw_password is not None:
|
||||
env["BW_PASSWORD"] = bw_password
|
||||
bw_path = _find_bw()
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
@@ -175,8 +183,13 @@ def setup_vault_routes():
|
||||
async def unlock(req: VaultUnlockRequest, request: Request):
|
||||
"""Unlock the vault and save the session key."""
|
||||
require_admin(request)
|
||||
# Pass the master password via the environment (--passwordenv), NOT as
|
||||
# an argv element — argv is visible to every local user through `ps` /
|
||||
# /proc/<pid>/cmdline. (The sibling /login handler already keeps the
|
||||
# password off argv by feeding it on stdin.)
|
||||
stdout, stderr, rc = await _run_bw(
|
||||
["unlock", req.master_password, "--raw"],
|
||||
["unlock", "--passwordenv", "BW_PASSWORD", "--raw"],
|
||||
bw_password=req.master_password,
|
||||
)
|
||||
if rc != 0:
|
||||
return {"ok": False, "error": f"Unlock failed: {stderr[:300]}"}
|
||||
|
||||
Reference in New Issue
Block a user