From 6cdbd528f3feaba205fc1a7dd3e0478c0540ac9e Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sun, 23 Aug 2026 00:13:10 -0400 Subject: [PATCH] [FIX] security: keep the master password out of the command line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redacting what we print treats the symptom. The password was still an argument, and /proc//cmdline is readable by EVERY user on the machine for as long as the command runs -- an exposure no filter reaches. It now travels in MASTER_PWD. The probe sets it on the child it spawns; once accepted it is placed in this process's environment, so every later call inherits it without argv ever carrying it. /proc//environ is readable only by its owner. Worth knowing for anyone reading the old code: the option was appended to every invocation, but odoo's db command reads it in the drop branch alone. list, restore and clone were carrying a secret they never used. The redaction stays. It is the last line, not the first, and other options still put secrets on command lines. --- FR --- Caviarder ce qu'on affiche traite le symptôme. Le mot de passe restait un argument, et /proc//cmdline est lisible par TOUT utilisateur de la machine tant que la commande tourne — une exposition qu'aucun filtre n'atteint. Il voyage désormais dans MASTER_PWD. La sonde le pose sur l'enfant qu'elle lance ; une fois accepté, il est placé dans l'environnement de ce processus, si bien que tous les appels suivants en héritent sans qu'argv le porte jamais. /proc//environ n'est lisible que par son propriétaire. À savoir pour qui relit l'ancien code : l'option était ajoutée à chaque invocation, alors que la commande db d'odoo ne la lit que dans la branche drop. list, restore et clone portaient un secret dont ils ne faisaient rien. Le caviardage reste. Il est le dernier rempart, pas le premier, et d'autres options mettent encore des secrets sur des lignes de commande. Assisted-by: Claude Opus 5 --- script/database/db_restore.py | 20 +++++++++++++++----- script/execute/execute.py | 13 +++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/script/database/db_restore.py b/script/database/db_restore.py index 9b30b67..a394f4f 100755 --- a/script/database/db_restore.py +++ b/script/database/db_restore.py @@ -104,17 +104,23 @@ def password_refused(sortie): return "AccessDenied" in (sortie or "") -def probe_master_password(arg_base): +def probe_master_password(arg_base, mot): """(accepté, sortie) — éprouver le mot de passe sur `--list`. La commande la plus inoffensive : elle ne touche à rien et rend le même refus qu'une restauration. Valider ici évite d'échouer à mi-parcours, une fois la base déjà supprimée. + + Le secret passe par l'environnement, jamais par argv : + /proc//cmdline est lisible par tout utilisateur de la machine. """ + env = os.environ.copy() + env["MASTER_PWD"] = mot done = subprocess.run( f"{arg_base} --list".split(" "), capture_output=True, text=True, + env=env, ) return done.returncode == 0, (done.stdout or "") + (done.stderr or "") @@ -133,8 +139,7 @@ def ask_master_password(arg_base, essais=MAX_ESSAIS_MOT_DE_PASSE): mot = get_master_password() if not mot: return None - candidat = f"{arg_base} --master_password={mot}" - accepte, sortie = probe_master_password(candidat) + accepte, sortie = probe_master_password(arg_base, mot) if accepte: return mot if not password_refused(sortie): @@ -256,7 +261,9 @@ def restore_or_clone(config, arg_base, cache_database, lst_db_cache): ) if config.neutralize: arg += " --neutralize" - # « arg_base » porte --master_password des que la base en exige un. + # Le secret ne traverse plus argv (il est dans MASTER_PWD), mais la + # commande peut porter d'autres options sensibles : on filtre quand + # même, le coût est nul et la garantie ne dépend alors d'aucun appelant. print(redact_secrets(arg)) print(redact_secrets(check_output(arg.split(" ")).decode())) if config.ignore_cache: @@ -290,7 +297,10 @@ def main(): if not master_password: _logger.error("Missing master password, cancel transaction.") sys.exit(1) - arg_base += f" --master_password={master_password}" + # Dans l'ENVIRONNEMENT, pas dans arg_base : tous les appels + # suivants sont des enfants de ce processus et en héritent, + # sans que le secret traverse jamais argv. + os.environ["MASTER_PWD"] = master_password else: _logger.info("No master password needed... Continue") diff --git a/script/execute/execute.py b/script/execute/execute.py index 386a243..a042323 100644 --- a/script/execute/execute.py +++ b/script/execute/execute.py @@ -19,10 +19,15 @@ except ModuleNotFoundError as e: VENV_ERPLIBRE = ".venv.erplibre" # Une commande construite ailleurs peut porter un secret en clair : todo.py et -# kdbx_manager.py y mettent « --default_password_auth '' », -# db_restore.py « --master_password=… ». Cette commande est affichée avant et -# après l'exécution, et journalisée en erreur : le secret finissait donc dans le -# terminal, dans les journaux et dans toute sortie CI qui les capture. +# kdbx_manager.py y mettent « --default_password_auth '' ». +# Cette commande est affichée avant et après l'exécution, et journalisée en +# erreur : le secret finissait donc dans le terminal, dans les journaux et dans +# toute sortie CI qui les capture. +# +# Ce filtre reste le dernier rempart, pas le premier : un secret n'a rien à +# faire sur argv, que /proc//cmdline expose à tout utilisateur de la +# machine et qu'aucun caviardage n'atteint. db_restore.py est passé à +# MASTER_PWD dans l'environnement pour cette raison. # # On caviarde la VALEUR, jamais le nom de l'option : la commande reste lisible et # reproductible, il ne manque que ce qui ne doit pas être lu.