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.