backup/restore: add option to use uncommon compression filter anyway

Previous commit introduced protection against uncommon (potentially
malicious) compression filters. This breaks restoring backups made with
a custom compression filter. Add an option to override this check, by
naming compression filter to use explicitly.
This commit is contained in:
Marek Marczykowski-Górecki 2019-09-08 02:39:01 +02:00
parent 10f15e6669
commit 14f77860bf
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
4 changed files with 32 additions and 5 deletions

View File

@ -72,6 +72,13 @@ Options
this limit and restore such (broken, or potentially malicious) backup
anyway.
.. option:: --compression-filter, -Z
Force specific compression filter, instead of the one named in the backup
header. The compression filter is a command that accepts ``-d`` option to
decompress data on stdin and output it to stdout. This can be used to
override built-in protection against uncommon compression.
.. option:: --dest-vm=APPVM, -d APPVM
Restore from a backup located in a specific AppVM

View File

@ -192,6 +192,11 @@ class BackupHeader(object):
else:
raise QubesException("Unrecognized header type")
if not header.validator(value):
if key == 'compression-filter':
raise QubesException(
"Unusual compression filter '{f}' found. Use "
"--compression-filter={f} to use it anyway.".format(
f=value))
raise QubesException("Invalid value for header: {}".format(key))
setattr(self, header.field, value)
@ -902,7 +907,8 @@ class BackupRestore(object):
self.subdir = subdir
self.username = os.path.basename(subdir)
def __init__(self, app, backup_location, backup_vm, passphrase):
def __init__(self, app, backup_location, backup_vm, passphrase,
force_compression_filter=None):
super(BackupRestore, self).__init__()
#: qubes.Qubes instance
@ -919,6 +925,10 @@ class BackupRestore(object):
#: backup path, inside VM pointed by :py:attr:`backup_vm`
self.backup_location = backup_location
#: force using specific application for (de)compression, instead of
#: the one named in the backup header
self.force_compression_filter = force_compression_filter
#: passphrase protecting backup integrity and optionally decryption
self.passphrase = passphrase
@ -1252,7 +1262,9 @@ class BackupRestore(object):
"failed). Is the password correct?")
filename = os.path.join(self.tmpdir, filename)
with open(filename, 'rb') as f_header:
header_data = BackupHeader(f_header.read())
header_data = BackupHeader(
f_header.read(),
compression_filter=self.force_compression_filter)
os.unlink(filename)
return header_data

View File

@ -58,7 +58,8 @@ class TC_00_qvm_backup_restore(qubesadmin.tests.QubesTestCase):
mock_handle_broken.assert_called_once_with(
self.app, mock.ANY, mock_restore_info)
mock_backup.assert_called_once_with(
self.app, '/some/path', None, 'testpass')
self.app, '/some/path', None, 'testpass',
force_compression_filter=None)
self.assertAllCalled()
@mock.patch('qubesadmin.tools.qvm_backup_restore.input', create=True)
@ -92,7 +93,8 @@ class TC_00_qvm_backup_restore(qubesadmin.tests.QubesTestCase):
qubesadmin.tools.qvm_backup_restore.main(['/some/path', 'test-vm'],
app=self.app)
mock_backup.assert_called_once_with(
self.app, '/some/path', None, 'testpass')
self.app, '/some/path', None, 'testpass',
force_compression_filter=None)
self.assertEqual(mock_backup.return_value.options.exclude, ['test-vm2'])
self.assertAllCalled()

View File

@ -72,6 +72,11 @@ parser.add_argument("--ignore-size-limit", action="store_true",
dest="ignore_size_limit", default=False,
help="Ignore size limit calculated from backup metadata")
parser.add_argument("--compression-filter", "-Z", action="store",
dest="compression",
help="Force specific compression filter program, "
"instead of the one from the backup header")
parser.add_argument("-d", "--dest-vm", action="store", dest="appvm",
help="Specify VM containing the backup to be restored")
@ -213,7 +218,8 @@ def main(args=None, app=None):
try:
backup = BackupRestore(args.app, args.backup_location,
appvm, passphrase)
appvm, passphrase,
force_compression_filter=args.compression)
except qubesadmin.exc.QubesException as e:
parser.error_runtime(str(e))
# unreachable - error_runtime will raise SystemExit