diff --git a/NEWS b/NEWS index 5a1bd387..dc62723c 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,7 @@ incident UI. See the documentation for more information: https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook * #936: Clarify Zabbix monitoring hook documentation about creating items. + * #1017: Fix a regression in which some MariaDB/MySQL passwords were not escaped correctly. 1.9.13 * #975: Add a "compression" option to the PostgreSQL database hook. diff --git a/borgmatic/hooks/data_source/mariadb.py b/borgmatic/hooks/data_source/mariadb.py index 8d54e12c..1f140793 100644 --- a/borgmatic/hooks/data_source/mariadb.py +++ b/borgmatic/hooks/data_source/mariadb.py @@ -65,10 +65,12 @@ def make_defaults_file_options(username=None, password=None, defaults_extra_file Do not use the returned value for multiple different command invocations. That will not work because each pipe is "used up" once read. ''' + escaped_password = None if password is None else password.replace('\\', '\\\\') + values = '\n'.join( ( (f'user={username}' if username is not None else ''), - (f"password='{password}'" if password is not None else ''), + (f'password="{escaped_password}"' if escaped_password is not None else ''), ) ).strip() diff --git a/tests/unit/hooks/data_source/test_mariadb.py b/tests/unit/hooks/data_source/test_mariadb.py index 4328f7e6..b3051783 100644 --- a/tests/unit/hooks/data_source/test_mariadb.py +++ b/tests/unit/hooks/data_source/test_mariadb.py @@ -36,7 +36,7 @@ def test_make_defaults_file_option_with_username_and_password_writes_them_to_fil flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor) flexmock(module.os).should_receive('write').with_args( - write_descriptor, b"[client]\nuser=root\npassword='trustsome1'" + write_descriptor, b'[client]\nuser=root\npassword="trustsome1"' ).once() flexmock(module.os).should_receive('close') flexmock(module.os).should_receive('set_inheritable') @@ -46,6 +46,22 @@ def test_make_defaults_file_option_with_username_and_password_writes_them_to_fil ) +def test_make_defaults_file_option_escapes_password_containing_backslash(): + read_descriptor = 99 + write_descriptor = flexmock() + + flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor) + flexmock(module.os).should_receive('write').with_args( + write_descriptor, b'[client]\nuser=root\n' + br'password="trust\\nsome1"' + ).once() + flexmock(module.os).should_receive('close') + flexmock(module.os).should_receive('set_inheritable') + + assert module.make_defaults_file_options(username='root', password=r'trust\nsome1') == ( + '--defaults-extra-file=/dev/fd/99', + ) + + def test_make_defaults_file_pipe_with_only_username_writes_it_to_file_descriptor(): read_descriptor = 99 write_descriptor = flexmock() @@ -68,7 +84,7 @@ def test_make_defaults_file_pipe_with_only_password_writes_it_to_file_descriptor flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor) flexmock(module.os).should_receive('write').with_args( - write_descriptor, b"[client]\npassword='trustsome1'" + write_descriptor, b'[client]\npassword="trustsome1"' ).once() flexmock(module.os).should_receive('close') flexmock(module.os).should_receive('set_inheritable') @@ -84,7 +100,7 @@ def test_make_defaults_file_option_with_defaults_extra_filename_includes_it_in_f flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor) flexmock(module.os).should_receive('write').with_args( - write_descriptor, b"!include extra.cnf\n[client]\nuser=root\npassword='trustsome1'" + write_descriptor, b'!include extra.cnf\n[client]\nuser=root\npassword="trustsome1"' ).once() flexmock(module.os).should_receive('close') flexmock(module.os).should_receive('set_inheritable')