From 22d30a1504edb491e4affc2e9fbbe801090977c6 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sat, 22 Aug 2026 23:39:00 -0400 Subject: [PATCH] [FIX] security: redact the master password from every output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL raised seven high-severity alerts on this branch. Three were real, and the same secret was behind all of them: the Odoo master password, which db_restore appends to its command line as soon as the database declares one. The command itself was printed raw -- "print(arg)" -- so the password reached stdout, and any terminal capture with it. The probe output was logged raw too, and a refused attempt echoes the command it tried. Wider than that: the runner filtered the command it was about to run, but not what came back. A tool that reprints its own arguments -- "set -x", a traceback, odoo_bin.sh -- put the secret straight back into the terminal AND into the log file the sink writes. Every subprocess line now goes through the same filter as the command. The four remaining alerts sit on expressions already wrapped in redact_secrets(). CodeQL does not cross re.sub, so it cannot see the barrier; the mitigation is real and they are false positives. --- FR --- CodeQL a levé sept alertes de sévérité haute sur cette branche. Trois étaient réelles, et le même secret était derrière : le mot de passe maître d'Odoo, que db_restore ajoute à sa ligne de commande dès que la base en exige un. La commande elle-même était imprimée telle quelle — « print(arg) » — donc le mot de passe atteignait la sortie standard, et toute capture de terminal avec elle. La sortie de la sonde était journalisée brute également, et un essai refusé réaffiche la commande tentée. Plus large : le lanceur filtrait la commande qu'il allait exécuter, mais pas ce qui en revenait. Un outil qui réaffiche ses propres arguments — « set -x », une trace, odoo_bin.sh — remettait le secret dans le terminal ET dans le fichier de journal. Chaque ligne du sous-processus passe désormais par le même filtre que la commande. Les quatre alertes restantes portent sur des expressions déjà entourées de redact_secrets(). CodeQL ne franchit pas re.sub et ne voit donc pas la barrière ; la mitigation est réelle, ce sont des faux positifs. Assisted-by: Claude Opus 5 --- script/database/db_restore.py | 15 +++++++++------ script/execute/execute.py | 5 +++++ script/qemu/deploy_qemu.py | 8 +++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/script/database/db_restore.py b/script/database/db_restore.py index 8cbac35..9b30b67 100755 --- a/script/database/db_restore.py +++ b/script/database/db_restore.py @@ -16,6 +16,8 @@ sys.path.append( os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")) ) +from script.execute.execute import redact_secrets + logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) _logger = logging.getLogger(__name__) @@ -138,7 +140,7 @@ def ask_master_password(arg_base, essais=MAX_ESSAIS_MOT_DE_PASSE): if not password_refused(sortie): # Autre chose est cassé : le dire, et ne pas noyer la panne # sous dix invites de mot de passe. - _logger.error(sortie.strip()[-1500:]) + _logger.error(redact_secrets(sortie.strip()[-1500:])) return None restants = essais - tour if restants: @@ -232,7 +234,7 @@ def restore_or_clone(config, arg_base, cache_database, lst_db_cache): f"{arg_base} --restore" f" --restore_image {config.image} --database {cache_database}" ) - print(check_output(arg.split(" ")).decode()) + print(redact_secrets(check_output(arg.split(" ")).decode())) verify_filestore(cache_database, config.image) if config.ignore_cache: @@ -254,8 +256,9 @@ def restore_or_clone(config, arg_base, cache_database, lst_db_cache): ) if config.neutralize: arg += " --neutralize" - print(arg) - print(check_output(arg.split(" ")).decode()) + # « arg_base » porte --master_password des que la base en exige un. + print(redact_secrets(arg)) + print(redact_secrets(check_output(arg.split(" ")).decode())) if config.ignore_cache: verify_filestore(config.database, config.image) @@ -298,7 +301,7 @@ def main(): for db in lst_db_cache: _logger.info(f"## Delete {db} ##") arg = f"{arg_base} --drop --database {db}" - out = check_output(arg.split(" ")).decode() + out = redact_secrets(check_output(arg.split(" ")).decode()) print(out) lst_db, lst_db_cache = get_list_db_cache(arg_base) @@ -308,7 +311,7 @@ def main(): if config.database in lst_db: _logger.info(f"## Drop {config.database} ##") arg = f"{arg_base} --drop --database {config.database}" - out = check_output(arg.split(" ")).decode() + out = redact_secrets(check_output(arg.split(" ")).decode()) print(out) if config.only_drop: return diff --git a/script/execute/execute.py b/script/execute/execute.py index dcb360f..386a243 100644 --- a/script/execute/execute.py +++ b/script/execute/execute.py @@ -183,6 +183,11 @@ class Execute: line = process.stdout.readline() if not line: break + # La sortie du sous-processus passe par le meme filtre que + # la commande : un outil qui reaffiche ses propres arguments + # (« set -x », une trace, odoo_bin.sh) y remettrait le secret + # que la ligne 165 venait d'ecarter. + line = redact_secrets(line) if not quiet: print(line, end="") if sink: diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index c253bf0..86f700e 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -2712,7 +2712,13 @@ def build_installer_initrd( ) with tempfile.TemporaryDirectory() as tmp: work = Path(tmp) - (work / "preseed.cfg").write_text(preseed, encoding="utf-8") + # Le mot de passe utilisateur y est HACHÉ (user-password-crypted). + # Reste celui de network-console, une valeur fixe et publique dont + # le composant est désactivé plus bas. Le répertoire temporaire est + # déjà en 0700 ; le mode explicite vaut pour qui lirait ce code. + cfg = work / "preseed.cfg" + cfg.touch(mode=0o600) + cfg.write_text(preseed, encoding="utf-8") members = ["preseed.cfg"] for path, _mode, content, _owner in guide or []: name = installer_guide_name(path)