summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjvoisin2019-10-12 16:13:49 -0700
committerjvoisin2019-10-12 16:13:49 -0700
commit5f0b3beb46d09af26107fe5f80e63ddccb127a59 (patch)
treef3d46e6e9dac60daa304d212bed62b17c019f7eb
parent3cef7fe7fc81c1495a461a8594b1df69467536ea (diff)
Add a way to disable the sandbox
Due to bubblewrap's pickiness, mat2 can now be run without a sandbox, even if bubblewrap is installed.
-rw-r--r--libmat2/abstract.py1
-rw-r--r--libmat2/bubblewrap.py (renamed from libmat2/subprocess.py)0
-rw-r--r--libmat2/exiftool.py22
-rw-r--r--libmat2/video.py12
-rwxr-xr-xmat214
-rw-r--r--tests/test_climat2.py31
-rw-r--r--tests/test_libmat2.py40
7 files changed, 100 insertions, 20 deletions
diff --git a/libmat2/abstract.py b/libmat2/abstract.py
index 8861966..5cfd0f2 100644
--- a/libmat2/abstract.py
+++ b/libmat2/abstract.py
@@ -32,6 +32,7 @@ class AbstractParser(abc.ABC):
32 32
33 self.output_filename = fname + '.cleaned' + extension 33 self.output_filename = fname + '.cleaned' + extension
34 self.lightweight_cleaning = False 34 self.lightweight_cleaning = False
35 self.sandbox = True
35 36
36 @abc.abstractmethod 37 @abc.abstractmethod
37 def get_meta(self) -> Dict[str, Union[str, dict]]: 38 def get_meta(self) -> Dict[str, Union[str, dict]]:
diff --git a/libmat2/subprocess.py b/libmat2/bubblewrap.py
index fb6fc9d..fb6fc9d 100644
--- a/libmat2/subprocess.py
+++ b/libmat2/bubblewrap.py
diff --git a/libmat2/exiftool.py b/libmat2/exiftool.py
index 024f490..89081e2 100644
--- a/libmat2/exiftool.py
+++ b/libmat2/exiftool.py
@@ -2,10 +2,11 @@ import functools
2import json 2import json
3import logging 3import logging
4import os 4import os
5import subprocess
5from typing import Dict, Union, Set 6from typing import Dict, Union, Set
6 7
7from . import abstract 8from . import abstract
8from . import subprocess 9from . import bubblewrap
9 10
10# Make pyflakes happy 11# Make pyflakes happy
11assert Set 12assert Set
@@ -19,9 +20,13 @@ class ExiftoolParser(abstract.AbstractParser):
19 meta_allowlist = set() # type: Set[str] 20 meta_allowlist = set() # type: Set[str]
20 21
21 def get_meta(self) -> Dict[str, Union[str, dict]]: 22 def get_meta(self) -> Dict[str, Union[str, dict]]:
22 out = subprocess.run([_get_exiftool_path(), '-json', self.filename], 23 if self.sandbox:
23 input_filename=self.filename, 24 out = bubblewrap.run([_get_exiftool_path(), '-json', self.filename],
24 check=True, stdout=subprocess.PIPE).stdout 25 input_filename=self.filename,
26 check=True, stdout=subprocess.PIPE).stdout
27 else:
28 out = subprocess.run([_get_exiftool_path(), '-json', self.filename],
29 check=True, stdout=subprocess.PIPE).stdout
25 meta = json.loads(out.decode('utf-8'))[0] 30 meta = json.loads(out.decode('utf-8'))[0]
26 for key in self.meta_allowlist: 31 for key in self.meta_allowlist:
27 meta.pop(key, None) 32 meta.pop(key, None)
@@ -48,9 +53,12 @@ class ExiftoolParser(abstract.AbstractParser):
48 '-o', self.output_filename, 53 '-o', self.output_filename,
49 self.filename] 54 self.filename]
50 try: 55 try:
51 subprocess.run(cmd, check=True, 56 if self.sandbox:
52 input_filename=self.filename, 57 bubblewrap.run(cmd, check=True,
53 output_filename=self.output_filename) 58 input_filename=self.filename,
59 output_filename=self.output_filename)
60 else:
61 subprocess.run(cmd, check=True)
54 except subprocess.CalledProcessError as e: # pragma: no cover 62 except subprocess.CalledProcessError as e: # pragma: no cover
55 logging.error("Something went wrong during the processing of %s: %s", self.filename, e) 63 logging.error("Something went wrong during the processing of %s: %s", self.filename, e)
56 return False 64 return False
diff --git a/libmat2/video.py b/libmat2/video.py
index 1492ba1..2b33bc0 100644
--- a/libmat2/video.py
+++ b/libmat2/video.py
@@ -1,3 +1,4 @@
1import subprocess
1import functools 2import functools
2import os 3import os
3import logging 4import logging
@@ -5,7 +6,7 @@ import logging
5from typing import Dict, Union 6from typing import Dict, Union
6 7
7from . import exiftool 8from . import exiftool
8from . import subprocess 9from . import bubblewrap
9 10
10 11
11class AbstractFFmpegParser(exiftool.ExiftoolParser): 12class AbstractFFmpegParser(exiftool.ExiftoolParser):
@@ -33,9 +34,12 @@ class AbstractFFmpegParser(exiftool.ExiftoolParser):
33 '-flags:a', '+bitexact', # don't add any metadata 34 '-flags:a', '+bitexact', # don't add any metadata
34 self.output_filename] 35 self.output_filename]
35 try: 36 try:
36 subprocess.run(cmd, check=True, 37 if self.sandbox:
37 input_filename=self.filename, 38 bubblewrap.run(cmd, check=True,
38 output_filename=self.output_filename) 39 input_filename=self.filename,
40 output_filename=self.output_filename)
41 else:
42 subprocess.run(cmd, check=True)
39 except subprocess.CalledProcessError as e: 43 except subprocess.CalledProcessError as e:
40 logging.error("Something went wrong during the processing of %s: %s", self.filename, e) 44 logging.error("Something went wrong during the processing of %s: %s", self.filename, e)
41 return False 45 return False
diff --git a/mat2 b/mat2
index b9f02f2..e67fea0 100755
--- a/mat2
+++ b/mat2
@@ -55,6 +55,8 @@ def create_arg_parser() -> argparse.ArgumentParser:
55 ', '.join(p.value for p in UnknownMemberPolicy)) 55 ', '.join(p.value for p in UnknownMemberPolicy))
56 parser.add_argument('--inplace', action='store_true', 56 parser.add_argument('--inplace', action='store_true',
57 help='clean in place, without backup') 57 help='clean in place, without backup')
58 parser.add_argument('--no-sandbox', dest='sandbox', action='store_true',
59 default=False, help='Disable bubblewrap\'s sandboxing.')
58 60
59 excl_group = parser.add_mutually_exclusive_group() 61 excl_group = parser.add_mutually_exclusive_group()
60 excl_group.add_argument('files', nargs='*', help='the files to process', 62 excl_group.add_argument('files', nargs='*', help='the files to process',
@@ -78,7 +80,7 @@ def create_arg_parser() -> argparse.ArgumentParser:
78 return parser 80 return parser
79 81
80 82
81def show_meta(filename: str): 83def show_meta(filename: str, sandbox: bool):
82 if not __check_file(filename): 84 if not __check_file(filename):
83 return 85 return
84 86
@@ -86,6 +88,7 @@ def show_meta(filename: str):
86 if p is None: 88 if p is None:
87 print("[-] %s's format (%s) is not supported" % (filename, mtype)) 89 print("[-] %s's format (%s) is not supported" % (filename, mtype))
88 return 90 return
91 p.sandbox = sandbox
89 __print_meta(filename, p.get_meta()) 92 __print_meta(filename, p.get_meta())
90 93
91 94
@@ -116,7 +119,7 @@ def __print_meta(filename: str, metadata: dict, depth: int = 1):
116 print(padding + " %s: harmful content" % k) 119 print(padding + " %s: harmful content" % k)
117 120
118 121
119def clean_meta(filename: str, is_lightweight: bool, inplace: bool, 122def clean_meta(filename: str, is_lightweight: bool, inplace: bool, sandbox: bool,
120 policy: UnknownMemberPolicy) -> bool: 123 policy: UnknownMemberPolicy) -> bool:
121 mode = (os.R_OK | os.W_OK) if inplace else os.R_OK 124 mode = (os.R_OK | os.W_OK) if inplace else os.R_OK
122 if not __check_file(filename, mode): 125 if not __check_file(filename, mode):
@@ -128,6 +131,7 @@ def clean_meta(filename: str, is_lightweight: bool, inplace: bool,
128 return False 131 return False
129 p.unknown_member_policy = policy 132 p.unknown_member_policy = policy
130 p.lightweight_cleaning = is_lightweight 133 p.lightweight_cleaning = is_lightweight
134 p.sandbox = sandbox
131 135
132 try: 136 try:
133 logging.debug('Cleaning %s…', filename) 137 logging.debug('Cleaning %s…', filename)
@@ -140,7 +144,6 @@ def clean_meta(filename: str, is_lightweight: bool, inplace: bool,
140 return False 144 return False
141 145
142 146
143
144def show_parsers(): 147def show_parsers():
145 print('[+] Supported formats:') 148 print('[+] Supported formats:')
146 formats = set() # Set[str] 149 formats = set() # Set[str]
@@ -171,6 +174,7 @@ def __get_files_recursively(files: List[str]) -> List[str]:
171 ret.add(f) 174 ret.add(f)
172 return list(ret) 175 return list(ret)
173 176
177
174def main() -> int: 178def main() -> int:
175 arg_parser = create_arg_parser() 179 arg_parser = create_arg_parser()
176 args = arg_parser.parse_args() 180 args = arg_parser.parse_args()
@@ -193,7 +197,7 @@ def main() -> int:
193 197
194 elif args.show: 198 elif args.show:
195 for f in __get_files_recursively(args.files): 199 for f in __get_files_recursively(args.files):
196 show_meta(f) 200 show_meta(f, args.sandbox)
197 return 0 201 return 0
198 202
199 else: 203 else:
@@ -210,7 +214,7 @@ def main() -> int:
210 futures = list() 214 futures = list()
211 for f in files: 215 for f in files:
212 future = executor.submit(clean_meta, f, args.lightweight, 216 future = executor.submit(clean_meta, f, args.lightweight,
213 inplace, policy) 217 inplace, args.sandbox, policy)
214 futures.append(future) 218 futures.append(future)
215 for future in concurrent.futures.as_completed(futures): 219 for future in concurrent.futures.as_completed(futures):
216 no_failure &= future.result() 220 no_failure &= future.result()
diff --git a/tests/test_climat2.py b/tests/test_climat2.py
index 6cf8a39..9d816b1 100644
--- a/tests/test_climat2.py
+++ b/tests/test_climat2.py
@@ -20,17 +20,17 @@ class TestHelp(unittest.TestCase):
20 def test_help(self): 20 def test_help(self):
21 proc = subprocess.Popen(mat2_binary + ['--help'], stdout=subprocess.PIPE) 21 proc = subprocess.Popen(mat2_binary + ['--help'], stdout=subprocess.PIPE)
22 stdout, _ = proc.communicate() 22 stdout, _ = proc.communicate()
23 self.assertIn(b'mat2 [-h] [-V] [--unknown-members policy] [--inplace] [-v] [-l]', 23 self.assertIn(b'mat2 [-h] [-V] [--unknown-members policy] [--inplace] [--no-sandbox]',
24 stdout) 24 stdout)
25 self.assertIn(b'[--check-dependencies] [-L | -s]', stdout) 25 self.assertIn(b' [-v] [-l] [--check-dependencies] [-L | -s]', stdout)
26 self.assertIn(b'[files [files ...]]', stdout) 26 self.assertIn(b'[files [files ...]]', stdout)
27 27
28 def test_no_arg(self): 28 def test_no_arg(self):
29 proc = subprocess.Popen(mat2_binary, stdout=subprocess.PIPE) 29 proc = subprocess.Popen(mat2_binary, stdout=subprocess.PIPE)
30 stdout, _ = proc.communicate() 30 stdout, _ = proc.communicate()
31 self.assertIn(b'mat2 [-h] [-V] [--unknown-members policy] [--inplace] [-v] [-l]', 31 self.assertIn(b'mat2 [-h] [-V] [--unknown-members policy] [--inplace] [--no-sandbox]',
32 stdout) 32 stdout)
33 self.assertIn(b'[--check-dependencies] [-L | -s]', stdout) 33 self.assertIn(b' [-v] [-l] [--check-dependencies] [-L | -s]', stdout)
34 self.assertIn(b'[files [files ...]]', stdout) 34 self.assertIn(b'[files [files ...]]', stdout)
35 35
36 36
@@ -40,12 +40,14 @@ class TestVersion(unittest.TestCase):
40 stdout, _ = proc.communicate() 40 stdout, _ = proc.communicate()
41 self.assertTrue(stdout.startswith(b'MAT2 ')) 41 self.assertTrue(stdout.startswith(b'MAT2 '))
42 42
43
43class TestDependencies(unittest.TestCase): 44class TestDependencies(unittest.TestCase):
44 def test_dependencies(self): 45 def test_dependencies(self):
45 proc = subprocess.Popen(mat2_binary + ['--check-dependencies'], stdout=subprocess.PIPE) 46 proc = subprocess.Popen(mat2_binary + ['--check-dependencies'], stdout=subprocess.PIPE)
46 stdout, _ = proc.communicate() 47 stdout, _ = proc.communicate()
47 self.assertTrue(b'MAT2' in stdout) 48 self.assertTrue(b'MAT2' in stdout)
48 49
50
49class TestReturnValue(unittest.TestCase): 51class TestReturnValue(unittest.TestCase):
50 def test_nonzero(self): 52 def test_nonzero(self):
51 ret = subprocess.call(mat2_binary + ['mat2'], stdout=subprocess.DEVNULL) 53 ret = subprocess.call(mat2_binary + ['mat2'], stdout=subprocess.DEVNULL)
@@ -112,6 +114,25 @@ class TestCleanMeta(unittest.TestCase):
112 114
113 os.remove('./tests/data/clean.jpg') 115 os.remove('./tests/data/clean.jpg')
114 116
117 def test_jpg_nosandbox(self):
118 shutil.copy('./tests/data/dirty.jpg', './tests/data/clean.jpg')
119
120 proc = subprocess.Popen(mat2_binary + ['--show', '--no-sandbox', './tests/data/clean.jpg'],
121 stdout=subprocess.PIPE)
122 stdout, _ = proc.communicate()
123 self.assertIn(b'Comment: Created with GIMP', stdout)
124
125 proc = subprocess.Popen(mat2_binary + ['./tests/data/clean.jpg'],
126 stdout=subprocess.PIPE)
127 stdout, _ = proc.communicate()
128
129 proc = subprocess.Popen(mat2_binary + ['--show', './tests/data/clean.cleaned.jpg'],
130 stdout=subprocess.PIPE)
131 stdout, _ = proc.communicate()
132 self.assertNotIn(b'Comment: Created with GIMP', stdout)
133
134 os.remove('./tests/data/clean.jpg')
135
115 136
116class TestIsSupported(unittest.TestCase): 137class TestIsSupported(unittest.TestCase):
117 def test_pdf(self): 138 def test_pdf(self):
@@ -181,6 +202,7 @@ class TestGetMeta(unittest.TestCase):
181 self.assertIn(b'i am a : various comment', stdout) 202 self.assertIn(b'i am a : various comment', stdout)
182 self.assertIn(b'artist: jvoisin', stdout) 203 self.assertIn(b'artist: jvoisin', stdout)
183 204
205
184class TestControlCharInjection(unittest.TestCase): 206class TestControlCharInjection(unittest.TestCase):
185 def test_jpg(self): 207 def test_jpg(self):
186 proc = subprocess.Popen(mat2_binary + ['--show', './tests/data/control_chars.jpg'], 208 proc = subprocess.Popen(mat2_binary + ['--show', './tests/data/control_chars.jpg'],
@@ -242,6 +264,7 @@ class TestCommandLineParallel(unittest.TestCase):
242 os.remove(path) 264 os.remove(path)
243 os.remove('./tests/data/dirty_%d.docx' % i) 265 os.remove('./tests/data/dirty_%d.docx' % i)
244 266
267
245class TestInplaceCleaning(unittest.TestCase): 268class TestInplaceCleaning(unittest.TestCase):
246 def test_cleaning(self): 269 def test_cleaning(self):
247 shutil.copy('./tests/data/dirty.jpg', './tests/data/clean.jpg') 270 shutil.copy('./tests/data/dirty.jpg', './tests/data/clean.jpg')
diff --git a/tests/test_libmat2.py b/tests/test_libmat2.py
index 13d861d..20e6a01 100644
--- a/tests/test_libmat2.py
+++ b/tests/test_libmat2.py
@@ -721,3 +721,43 @@ class TestCleaningArchives(unittest.TestCase):
721 os.remove('./tests/data/dirty.tar.xz') 721 os.remove('./tests/data/dirty.tar.xz')
722 os.remove('./tests/data/dirty.cleaned.tar.xz') 722 os.remove('./tests/data/dirty.cleaned.tar.xz')
723 os.remove('./tests/data/dirty.cleaned.cleaned.tar.xz') 723 os.remove('./tests/data/dirty.cleaned.cleaned.tar.xz')
724
725class TestNoSandbox(unittest.TestCase):
726 def test_avi_nosandbox(self):
727 shutil.copy('./tests/data/dirty.avi', './tests/data/clean.avi')
728 p = video.AVIParser('./tests/data/clean.avi')
729 p.sandbox = False
730
731 meta = p.get_meta()
732 self.assertEqual(meta['Software'], 'MEncoder SVN-r33148-4.0.1')
733
734 ret = p.remove_all()
735 self.assertTrue(ret)
736
737 p = video.AVIParser('./tests/data/clean.cleaned.avi')
738 self.assertEqual(p.get_meta(), {})
739 self.assertTrue(p.remove_all())
740
741 os.remove('./tests/data/clean.avi')
742 os.remove('./tests/data/clean.cleaned.avi')
743 os.remove('./tests/data/clean.cleaned.cleaned.avi')
744
745 def test_png_nosandbox(self):
746 shutil.copy('./tests/data/dirty.png', './tests/data/clean.png')
747 p = images.PNGParser('./tests/data/clean.png')
748 p.sandbox = False
749 p.lightweight_cleaning = True
750
751 meta = p.get_meta()
752 self.assertEqual(meta['Comment'], 'This is a comment, be careful!')
753
754 ret = p.remove_all()
755 self.assertTrue(ret)
756
757 p = images.PNGParser('./tests/data/clean.cleaned.png')
758 self.assertEqual(p.get_meta(), {})
759 self.assertTrue(p.remove_all())
760
761 os.remove('./tests/data/clean.png')
762 os.remove('./tests/data/clean.cleaned.png')
763 os.remove('./tests/data/clean.cleaned.cleaned.png')