Files
docker-volume-backup/src/baudolo/restore/db/mariadb.py
T
kevinveenbirkenbachandClaude Opus 5.5 328520169b fix(security): keep database passwords out of every argv
MariaDB dumps and restores read the password from MYSQL_PWD, and docker exec forwards env variables by name only, so no process listing shows a password. baudolo-restore gains --db-password-stdin; --db-password still works but warns.

🎯 Fix: 96 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: ~100 %
🏆 Score: 93
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:37:04 +00:00

107 lines
2.9 KiB
Python

from __future__ import annotations
import sys
from pathlib import Path
from baudolo.restore.run import docker_exec, docker_exec_sh
from .version import guard
_NO_CLIENT = "ERROR: neither 'mariadb' nor 'mysql' found in container."
def _pick_client(container: str) -> str:
"""
Prefer 'mariadb', fallback to 'mysql'.
Some MariaDB images no longer ship a 'mysql' binary, so we must not assume it exists.
"""
script = r"""
set -eu
if command -v mariadb >/dev/null 2>&1; then echo mariadb; exit 0; fi
if command -v mysql >/dev/null 2>&1; then echo mysql; exit 0; fi
exit 42
"""
try:
out = docker_exec_sh(container, script, capture=True).stdout.decode().strip()
except Exception:
print(_NO_CLIENT, file=sys.stderr)
raise
if not out:
print(_NO_CLIENT, file=sys.stderr)
raise RuntimeError("empty client detection output")
return out
def restore_mariadb_sql(
*,
container: str,
db_name: str,
user: str,
password: str,
sql_path: str,
empty: bool,
check_version: bool = True,
) -> None:
client = _pick_client(container)
if not Path(sql_path).is_file():
raise FileNotFoundError(sql_path)
docker_env = {"MYSQL_PWD": password}
if check_version:
guard(
sql_path=sql_path,
engine="mariadb",
container=container,
user=user,
password=password,
client=client,
)
if empty:
# Do not hardcode 'mysql': MariaDB 11 images may not ship that binary.
result = docker_exec(
container,
[
client,
"-u",
user,
"-N",
"-e",
f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{db_name}';", # noqa: S608 - validate_database() constrains the name to ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$
],
capture=True,
docker_env=docker_env,
)
tables = result.stdout.decode().split()
if tables:
# SET FOREIGN_KEY_CHECKS is session-scoped, so it must share one
# client session with the DROPs or FK constraints still fire.
drop_sql = (
"SET FOREIGN_KEY_CHECKS=0; "
+ " ".join(
f"DROP TABLE IF EXISTS `{db_name}`.`{tbl}`;" for tbl in tables
)
+ " SET FOREIGN_KEY_CHECKS=1;"
)
docker_exec(
container,
[
client,
"-u",
user,
"-e",
drop_sql,
],
docker_env=docker_env,
)
with Path(sql_path).open("rb") as f:
docker_exec(
container, [client, "-u", user, db_name], stdin=f, docker_env=docker_env
)
print(f"MariaDB/MySQL restore complete for db '{db_name}'.")