diff --git a/attic/archive.py b/attic/archive.py index d001d4e66..4e8842915 100644 --- a/attic/archive.py +++ b/attic/archive.py @@ -11,7 +11,7 @@ from . import xattr from .chunker import chunkify from .helpers import uid2user, user2uid, gid2group, group2gid, \ - Statistics, decode_dict, st_mtime_ns + Statistics, decode_dict, st_mtime_ns, make_path_safe ITEMS_BUFFER = 1024 * 1024 CHUNK_MIN = 1024 @@ -223,7 +223,8 @@ def add(id): def extract_item(self, item, restore_attrs=True, peek=None): dest = self.cwd - assert item[b'path'][:1] not in ('/', '\\', ':') + if item[b'path'].startswith('/') or item[b'path'].startswith('..'): + raise Exception('Path should be relative and local') path = os.path.join(dest, item[b'path']) # Attempt to remove existing files, ignore errors on failure try: @@ -355,23 +356,23 @@ def stat_attrs(self, st, path): return item def process_item(self, path, st): - item = {b'path': path.lstrip('/\\:')} + item = {b'path': make_path_safe(path)} item.update(self.stat_attrs(st, path)) self.add_item(item) def process_dev(self, path, st): - item = {b'path': path.lstrip('/\\:'), b'rdev': st.st_rdev} + item = {b'path': make_path_safe(path), b'rdev': st.st_rdev} item.update(self.stat_attrs(st, path)) self.add_item(item) def process_symlink(self, path, st): source = os.readlink(path) - item = {b'path': path.lstrip('/\\:'), b'source': source} + item = {b'path': make_path_safe(path), b'source': source} item.update(self.stat_attrs(st, path)) self.add_item(item) def process_file(self, path, st, cache): - safe_path = path.lstrip('/\\:') + safe_path = make_path_safe(path) # Is it a hard link? if st.st_nlink > 1: source = self.hard_links.get((st.st_ino, st.st_dev)) diff --git a/attic/archiver.py b/attic/archiver.py index cc8739b0e..a0ce2c40f 100644 --- a/attic/archiver.py +++ b/attic/archiver.py @@ -91,6 +91,7 @@ def do_create(self, args): except IOError: pass for path in args.paths: + path = os.path.normpath(path) if args.dontcross: try: restrict_dev = os.lstat(path).st_dev diff --git a/attic/helpers.py b/attic/helpers.py index 23358f5ad..b5241eb4b 100644 --- a/attic/helpers.py +++ b/attic/helpers.py @@ -362,6 +362,15 @@ def remove_surrogates(s, errors='replace'): return s.encode('utf-8', errors).decode('utf-8') +_safe_re = re.compile('^((..)?/+)+') + + +def make_path_safe(path): + """Make path safe by making it relative and local + """ + return _safe_re.sub('', path) or '.' + + def daemonize(): """Detach process from controlling terminal and run in background """ diff --git a/attic/testsuite/__init__.py b/attic/testsuite/__init__.py index 74e0c9c8a..9dfb27d38 100644 --- a/attic/testsuite/__init__.py +++ b/attic/testsuite/__init__.py @@ -32,6 +32,8 @@ class AtticTestCase(unittest.TestCase): """ """ + assert_in = unittest.TestCase.assertIn + assert_not_in = unittest.TestCase.assertNotIn assert_equal = unittest.TestCase.assertEqual assert_not_equal = unittest.TestCase.assertNotEqual assert_raises = unittest.TestCase.assertRaises diff --git a/attic/testsuite/archiver.py b/attic/testsuite/archiver.py index 56d45277a..ec3d5857c 100644 --- a/attic/testsuite/archiver.py +++ b/attic/testsuite/archiver.py @@ -157,6 +157,15 @@ def test_extract_include_exclude(self): self.attic('extract', '--exclude=input/file2', self.repository_location + '::test') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3']) + def test_path_normalization(self): + self.attic('init', self.repository_location) + self.create_regual_file('dir1/dir2/file', size=1024 * 80) + with changedir('input/dir1/dir2'): + self.attic('create', self.repository_location + '::test', '../../../input/dir1/../dir1/dir2/..') + output = self.attic('list', self.repository_location + '::test') + self.assert_not_in('..', output) + self.assert_in(' input/dir1/dir2/file', output) + def test_overwrite(self): self.create_regual_file('file1', size=1024 * 80) self.create_regual_file('dir2/file2', size=1024 * 80) diff --git a/attic/testsuite/helpers.py b/attic/testsuite/helpers.py index 61c56c6d3..892eb8e13 100644 --- a/attic/testsuite/helpers.py +++ b/attic/testsuite/helpers.py @@ -1,5 +1,5 @@ from datetime import datetime -from attic.helpers import Location, format_timedelta, IncludePattern, ExcludePattern +from attic.helpers import Location, format_timedelta, IncludePattern, ExcludePattern, make_path_safe from attic.testsuite import AtticTestCase @@ -54,3 +54,15 @@ def test(self): self.assert_equal(ExcludePattern('/tmp').match('/tmp'), True) self.assert_equal(ExcludePattern('/tmp').match('/tmp/foo'), True) self.assert_equal(ExcludePattern('/tmp').match('/tmofoo'), False) + + +class MakePathSafeTestCase(AtticTestCase): + + def test(self): + self.assert_equal(make_path_safe('/foo/bar'), 'foo/bar') + self.assert_equal(make_path_safe('/foo/bar'), 'foo/bar') + self.assert_equal(make_path_safe('../foo/bar'), 'foo/bar') + self.assert_equal(make_path_safe('../../foo/bar'), 'foo/bar') + self.assert_equal(make_path_safe('/'), '.') + self.assert_equal(make_path_safe('/'), '.') +