erplibre/script/database/db_restore.py

356 lines
12 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import argparse
import configparser
import getpass
2022-01-24 01:39:49 -05:00
import logging
import os
[ADD] filestore: purge once at the end, tidy at the restore, see the 30 MB Not between bumps, and the measurement says why: two to eleven fields vanish at one step and COME BACK at the next -- hr.employee.phone, account.move.statement_id. "The field is gone" is a transient state while a migration runs. And there would be nothing to gain: 1881 dead rows appear at the 13 bump and the count never moves again, so one final pass takes them all. The nesting is born once, at the restore, and the clone copies it identically into every step -- the six databases carried the same 1168 files. It is offered where it is born, never on a closed stdin. Widened too: the tool was named after missing files and so looked only at those. 1860 rows in 18 hold a live file for a field that is gone -- 31 MB of res.partner.image and thumbnails from before Odoo 13 computed them. Odoo's collector will never touch them while the row exists. --- FR --- Pas entre les paliers, et la mesure dit pourquoi : deux à onze champs disparaissent à une étape et REVIENNENT à la suivante -- hr.employee.phone, account.move.statement_id. « Le champ n'existe plus » est transitoire tant que la migration court. Et il n'y aurait rien à y gagner : 1881 lignes mortes naissent au palier 13 et le compte ne bouge plus, donc une passe finale les prend toutes. Le nichage naît une fois, à la restauration, et le clone le recopie partout — les six bases portaient les mêmes 1168 fichiers. Il se répare là où il naît, jamais sur un stdin fermé. Élargi aussi : l'outil portait le nom des fichiers absents et ne regardait donc qu'eux. 1860 lignes en 18 retiennent un fichier bien présent pour un champ disparu — 31 Mo d'images res.partner et de vignettes d'avant qu'Odoo 13 ne les calcule. Assisted-by: Claude Opus 5
2026-08-22 00:06:25 -04:00
import shutil
import subprocess
import sys
import uuid
from subprocess import check_output
sys.path.append(
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
)
[FIX] security: redact the master password from every output 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
2026-08-22 23:39:00 -04:00
from script.execute.execute import redact_secrets
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
_logger = logging.getLogger(__name__)
def get_config():
"""Parse command line arguments, extracting the config file name,
returning the union of config file and command line arguments
:return: dict of config file settings and command line arguments
"""
# TODO update description
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""\
DESCRIPTION
Restore database, use cache to clone to improve speed.
SUGGESTION
./script/database/db_restore.py -d test
""",
epilog="""\
""",
)
# parser.add_argument('-d', '--dir', dest="dir", default="./",
# help="Path of repo to change remote, including submodule.")
parser.add_argument("-d", "--database", help="Database to manipulate.")
parser.add_argument(
"--image",
help=(
"Image name to restore, from directory image_db, filename without"
" '.zip'. Example, use odoo12.0_base to use image"
" odoo12.0_base.zip. Default value is odoo12.0_base"
),
)
parser.add_argument(
"--clean_cache",
action="store_true",
help="Delete all database cache to clone, begin by _cache_.",
)
parser.add_argument(
"--ignore_cache",
action="store_true",
help="Ignore creating _cache_ when restoring.",
)
parser.add_argument(
"--only_drop",
action="store_true",
help="Will only drop database if exist.",
)
parser.add_argument(
"--neutralize",
action="store_true",
help="Will disable all cron.",
)
args = parser.parse_args()
return args
def get_master_password():
try:
# _logger.info("You have 5 seconds to add master password...")
pa = getpass.getpass(prompt="\nEnter master password... ")
return pa
except getpass.GetPassWarning:
_logger.error("Password echoed, danger!")
# Assez pour une faute de frappe répétée, pas assez pour qu'une boucle
# oubliée tourne toute la nuit devant une invite que personne ne lit.
MAX_ESSAIS_MOT_DE_PASSE = 10
def password_refused(sortie):
"""Odoo a-t-il refusé le mot de passe maître, ou autre chose ?
La distinction porte tout. Reposer la question sur n'importe quel
échec cacherait la vraie panne derrière dix invites, et l'on
chercherait un mot de passe alors que la base est cassée.
Odoo lève `AccessDenied` — la classe apparaît dans la trace, et son
message traduit peut varier. On reconnaît donc la CLASSE.
"""
return "AccessDenied" in (sortie or "")
def probe_name():
"""Un nom de base qui ne peut appartenir à personne.
La sonde DEMANDE une suppression : si le nom désignait une vraie
base et que le mot de passe était bon, on la perdrait. Un uuid4 rend
la collision impossible en pratique, et le préfixe dit d'où il vient
à qui le verrait passer dans un journal.
"""
return f"el_probe_{uuid.uuid4().hex}"
[FIX] security: keep the master password out of the command line Redacting what we print treats the symptom. The password was still an argument, and /proc/<pid>/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/<pid>/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/<pid>/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/<pid>/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
2026-08-23 00:13:10 -04:00
def probe_master_password(arg_base, mot):
"""(accepté, sortie) — éprouver le mot de passe pour de vrai.
`--list` ne lit JAMAIS le mot de passe : mesuré,
`MASTER_PWD="ceci_est_faux" odoo-bin db --list` sort en 0. La sonde
d'avant acceptait donc le premier mot saisi, juste ou faux, et la
boucle des dix essais ne servait à rien — le refus n'arrivait qu'au
`--restore`, une fois la base déjà supprimée.
Seule l'action `drop` consulte le mot de passe, et elle le fait
AVANT de regarder la base : `check_super` d'abord, `db_exists`
ensuite. Sur un nom qui n'existe pas, elle ne touche donc rien et
répond quand même. Mesuré sur la machine d'essai : mauvais mot de
passe → code 1 et `AccessDenied` dans la trace ; bon mot de passe →
code 0, silence, et les huit bases toujours là.
[FIX] security: keep the master password out of the command line Redacting what we print treats the symptom. The password was still an argument, and /proc/<pid>/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/<pid>/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/<pid>/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/<pid>/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
2026-08-23 00:13:10 -04:00
Le secret passe par l'environnement, jamais par argv :
/proc/<pid>/cmdline est lisible par tout utilisateur de la machine.
"""
[FIX] security: keep the master password out of the command line Redacting what we print treats the symptom. The password was still an argument, and /proc/<pid>/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/<pid>/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/<pid>/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/<pid>/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
2026-08-23 00:13:10 -04:00
env = os.environ.copy()
env["MASTER_PWD"] = mot
done = subprocess.run(
f"{arg_base} --drop --database {probe_name()}".split(" "),
capture_output=True,
text=True,
[FIX] security: keep the master password out of the command line Redacting what we print treats the symptom. The password was still an argument, and /proc/<pid>/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/<pid>/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/<pid>/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/<pid>/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
2026-08-23 00:13:10 -04:00
env=env,
)
return done.returncode == 0, (done.stdout or "") + (done.stderr or "")
def ask_master_password(arg_base, essais=MAX_ESSAIS_MOT_DE_PASSE):
"""Le mot de passe maître, redemandé tant qu'Odoo le refuse.
None si l'on renonce — invite vide, essais épuisés, ou panne qui
n'a rien à voir avec le mot de passe.
Une faute de frappe arrêtait la migration net, sur une trace
`CalledProcessError` que rien n'attrapait. Après une heure de
paliers, c'est cher payé pour une lettre.
"""
for tour in range(1, essais + 1):
mot = get_master_password()
if not mot:
return None
[FIX] security: keep the master password out of the command line Redacting what we print treats the symptom. The password was still an argument, and /proc/<pid>/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/<pid>/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/<pid>/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/<pid>/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
2026-08-23 00:13:10 -04:00
accepte, sortie = probe_master_password(arg_base, mot)
if accepte:
return mot
if not password_refused(sortie):
# Autre chose est cassé : le dire, et ne pas noyer la panne
# sous dix invites de mot de passe.
[FIX] security: redact the master password from every output 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
2026-08-22 23:39:00 -04:00
_logger.error(redact_secrets(sortie.strip()[-1500:]))
return None
restants = essais - tour
if restants:
_logger.warning(
f"Master password refused, {restants} attempt(s) left."
)
_logger.error("Master password refused too many times.")
return None
def get_list_db_cache(arg_base):
arg = f"{arg_base} --list"
out = check_output(arg.split(" ")).decode()
lst_db = out.strip().split("\n")
lst_db_cache = [a for a in lst_db if a.startswith("_cache_")]
return lst_db, lst_db_cache
def verify_filestore(database, image):
"""Contrôler qu'une restauration a bien posé ses fichiers.
Une seule fois, à la restauration d'origine. Après un clone il n'y a
rien à vérifier : `copytree` recopie la source telle quelle, défauts
compris — le contrôle appartient à ce qui a créé le défaut, pas à ce
qui l'a dupliqué.
Le contrôle n'interrompt pas : la base est restaurée et utilisable,
c'est la DISPOSITION des fichiers qui est suspecte. Refuser ici
casserait des chaînes qui marchent, pour un défaut qui se répare
d'une commande.
"""
chemin = os.path.join("image_db", f"{image}.zip")
if not os.path.isfile(chemin):
return
try:
from script.analyse import check_filestore
except Exception: # pragma: no cover - l'outil d'analyse est optionnel
return
rapport = check_filestore.verify_restore(database, chemin)
for ligne in check_filestore.render_verify(rapport):
print(ligne)
[ADD] filestore: purge once at the end, tidy at the restore, see the 30 MB Not between bumps, and the measurement says why: two to eleven fields vanish at one step and COME BACK at the next -- hr.employee.phone, account.move.statement_id. "The field is gone" is a transient state while a migration runs. And there would be nothing to gain: 1881 dead rows appear at the 13 bump and the count never moves again, so one final pass takes them all. The nesting is born once, at the restore, and the clone copies it identically into every step -- the six databases carried the same 1168 files. It is offered where it is born, never on a closed stdin. Widened too: the tool was named after missing files and so looked only at those. 1860 rows in 18 hold a live file for a field that is gone -- 31 MB of res.partner.image and thumbnails from before Odoo 13 computed them. Odoo's collector will never touch them while the row exists. --- FR --- Pas entre les paliers, et la mesure dit pourquoi : deux à onze champs disparaissent à une étape et REVIENNENT à la suivante -- hr.employee.phone, account.move.statement_id. « Le champ n'existe plus » est transitoire tant que la migration court. Et il n'y aurait rien à y gagner : 1881 lignes mortes naissent au palier 13 et le compte ne bouge plus, donc une passe finale les prend toutes. Le nichage naît une fois, à la restauration, et le clone le recopie partout — les six bases portaient les mêmes 1168 fichiers. Il se répare là où il naît, jamais sur un stdin fermé. Élargi aussi : l'outil portait le nom des fichiers absents et ne regardait donc qu'eux. 1860 lignes en 18 retiennent un fichier bien présent pour un champ disparu — 31 Mo d'images res.partner et de vignettes d'avant qu'Odoo 13 ne les calcule. Assisted-by: Claude Opus 5
2026-08-22 00:06:25 -04:00
if rapport.get("nested"):
offer_tidy(check_filestore, rapport)
def offer_tidy(check_filestore, rapport):
"""Proposer de ranger TOUT DE SUITE, là où le défaut naît.
C'est le seul endroit qui vaille. Le nichage se produit une fois, à
la restauration, puis le clone le recopie tel quel : mesuré, les six
bases de la chaîne portaient les mêmes 1168 fichiers. Ranger ici,
c'est ranger une fois ; ranger plus tard, c'est six fois.
Rien ne se fait sans réponse humaine, et rien du tout hors d'un
terminal : ce script tourne aussi sans personne devant, et une
question posée à un `stdin` fermé arrêterait la migration.
"""
if not sys.stdin.isatty():
return
remonter, doublons = check_filestore.tidy_nested_plan(rapport)
if not remonter and not doublons:
return
print(f" {len(remonter)} à remonter, {len(doublons)} doublons purs")
try:
reponse = input("💬 Ranger maintenant ? (y/N) : ").strip().lower()
except EOFError:
return
if reponse not in ("y", "yes", "o"):
return
for source, cible in remonter:
os.makedirs(os.path.dirname(cible), exist_ok=True)
shutil.move(source, cible)
for source, _cible in doublons:
os.remove(source)
shutil.rmtree(check_filestore.nested_dir(rapport), ignore_errors=True)
print(f"✅ {len(remonter)} remontés, {len(doublons)} doublons supprimés.")
def restore_or_clone(config, arg_base, cache_database, lst_db_cache):
"""Restaurer depuis l'image, ou cloner le cache déjà restauré.
Le contrôle du filestore ne suit QUE les vraies restaurations. Le
clone recopie sa source telle quelle : contrôler le miroir dirait
deux fois la même chose, et la seconde au mauvais endroit.
"""
if cache_database not in lst_db_cache and not config.ignore_cache:
_logger.info(
f"## Create cache {cache_database} from image {config.image} ##"
)
arg = (
f"{arg_base} --restore"
f" --restore_image {config.image} --database {cache_database}"
)
[FIX] security: redact the master password from every output 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
2026-08-22 23:39:00 -04:00
print(redact_secrets(check_output(arg.split(" ")).decode()))
verify_filestore(cache_database, config.image)
if config.ignore_cache:
_logger.info(
f"## Restoring {config.image} to database {config.database} ##"
)
arg = (
f"{arg_base} --restore --restore_image"
f" {config.image} --database {config.database}"
)
else:
_logger.info(
f"## Clone cache {cache_database} to database {config.database} ##"
)
arg = (
f"{arg_base} --clone --from_database"
f" {cache_database} --database {config.database}"
)
if config.neutralize:
arg += " --neutralize"
[FIX] security: keep the master password out of the command line Redacting what we print treats the symptom. The password was still an argument, and /proc/<pid>/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/<pid>/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/<pid>/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/<pid>/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
2026-08-23 00:13:10 -04:00
# 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.
[FIX] security: redact the master password from every output 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
2026-08-22 23:39:00 -04:00
print(redact_secrets(arg))
print(redact_secrets(check_output(arg.split(" ")).decode()))
if config.ignore_cache:
verify_filestore(config.database, config.image)
def main():
config = get_config()
arg_base = "./odoo_bin.sh db"
if not config.image:
with open(".odoo-version", "r") as f:
odoo_version = f.readline()
config.image = f"odoo{odoo_version}_base"
# check if it needs master password from config file
has_config_file = True
config_path = "./config.conf"
if not os.path.isfile(config_path):
config_path = "/etc/odoo/odoo.conf"
if not os.path.isfile(config_path):
has_config_file = False
if has_config_file:
config_parser = configparser.ConfigParser()
config_parser.read(config_path)
has_admin_password = config_parser.get("options", "admin_passwd")
if has_admin_password and has_admin_password != "admin":
master_password = ask_master_password(arg_base)
if not master_password:
_logger.error("Missing master password, cancel transaction.")
sys.exit(1)
[FIX] security: keep the master password out of the command line Redacting what we print treats the symptom. The password was still an argument, and /proc/<pid>/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/<pid>/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/<pid>/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/<pid>/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
2026-08-23 00:13:10 -04:00
# 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")
# Get list of database
lst_db, lst_db_cache = get_list_db_cache(arg_base)
if config.clean_cache:
for db in lst_db_cache:
_logger.info(f"## Delete {db} ##")
arg = f"{arg_base} --drop --database {db}"
[FIX] security: redact the master password from every output 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
2026-08-22 23:39:00 -04:00
out = redact_secrets(check_output(arg.split(" ")).decode())
print(out)
lst_db, lst_db_cache = get_list_db_cache(arg_base)
if config.database:
cache_database = f"_cache_{config.image}"
# Drop db
if config.database in lst_db:
_logger.info(f"## Drop {config.database} ##")
arg = f"{arg_base} --drop --database {config.database}"
[FIX] security: redact the master password from every output 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
2026-08-22 23:39:00 -04:00
out = redact_secrets(check_output(arg.split(" ")).decode())
print(out)
if config.only_drop:
return
restore_or_clone(config, arg_base, cache_database, lst_db_cache)
if not config.clean_cache and not config.database:
print("Nothing to do.")
if __name__ == "__main__":
main()