Ver Fonte

Make sure all paths included in an archive are relative and local

Jonas Borgström há 12 anos atrás
pai
commit
af059fbdfc

+ 7 - 6
attic/archive.py

@@ -11,7 +11,7 @@ from io import BytesIO
 from . import xattr
 from . import xattr
 from .chunker import chunkify
 from .chunker import chunkify
 from .helpers import uid2user, user2uid, gid2group, group2gid, \
 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
 ITEMS_BUFFER = 1024 * 1024
 CHUNK_MIN = 1024
 CHUNK_MIN = 1024
@@ -223,7 +223,8 @@ class Archive(object):
 
 
     def extract_item(self, item, restore_attrs=True, peek=None):
     def extract_item(self, item, restore_attrs=True, peek=None):
         dest = self.cwd
         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'])
         path = os.path.join(dest, item[b'path'])
         # Attempt to remove existing files, ignore errors on failure
         # Attempt to remove existing files, ignore errors on failure
         try:
         try:
@@ -355,23 +356,23 @@ class Archive(object):
         return item
         return item
 
 
     def process_item(self, path, st):
     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))
         item.update(self.stat_attrs(st, path))
         self.add_item(item)
         self.add_item(item)
 
 
     def process_dev(self, path, st):
     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))
         item.update(self.stat_attrs(st, path))
         self.add_item(item)
         self.add_item(item)
 
 
     def process_symlink(self, path, st):
     def process_symlink(self, path, st):
         source = os.readlink(path)
         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))
         item.update(self.stat_attrs(st, path))
         self.add_item(item)
         self.add_item(item)
 
 
     def process_file(self, path, st, cache):
     def process_file(self, path, st, cache):
-        safe_path = path.lstrip('/\\:')
+        safe_path = make_path_safe(path)
         # Is it a hard link?
         # Is it a hard link?
         if st.st_nlink > 1:
         if st.st_nlink > 1:
             source = self.hard_links.get((st.st_ino, st.st_dev))
             source = self.hard_links.get((st.st_ino, st.st_dev))

+ 1 - 0
attic/archiver.py

@@ -91,6 +91,7 @@ class Archiver:
             except IOError:
             except IOError:
                 pass
                 pass
         for path in args.paths:
         for path in args.paths:
+            path = os.path.normpath(path)
             if args.dontcross:
             if args.dontcross:
                 try:
                 try:
                     restrict_dev = os.lstat(path).st_dev
                     restrict_dev = os.lstat(path).st_dev

+ 9 - 0
attic/helpers.py

@@ -362,6 +362,15 @@ def remove_surrogates(s, errors='replace'):
     return s.encode('utf-8', errors).decode('utf-8')
     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():
 def daemonize():
     """Detach process from controlling terminal and run in background
     """Detach process from controlling terminal and run in background
     """
     """

+ 2 - 0
attic/testsuite/__init__.py

@@ -32,6 +32,8 @@ utime_supports_fd = os.utime in getattr(os, 'supports_fd', {})
 class AtticTestCase(unittest.TestCase):
 class AtticTestCase(unittest.TestCase):
     """
     """
     """
     """
+    assert_in = unittest.TestCase.assertIn
+    assert_not_in = unittest.TestCase.assertNotIn
     assert_equal = unittest.TestCase.assertEqual
     assert_equal = unittest.TestCase.assertEqual
     assert_not_equal = unittest.TestCase.assertNotEqual
     assert_not_equal = unittest.TestCase.assertNotEqual
     assert_raises = unittest.TestCase.assertRaises
     assert_raises = unittest.TestCase.assertRaises

+ 9 - 0
attic/testsuite/archiver.py

@@ -157,6 +157,15 @@ class ArchiverTestCase(AtticTestCase):
             self.attic('extract', '--exclude=input/file2', self.repository_location + '::test')
             self.attic('extract', '--exclude=input/file2', self.repository_location + '::test')
         self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
         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):
     def test_overwrite(self):
         self.create_regual_file('file1', size=1024 * 80)
         self.create_regual_file('file1', size=1024 * 80)
         self.create_regual_file('dir2/file2', size=1024 * 80)
         self.create_regual_file('dir2/file2', size=1024 * 80)

+ 13 - 1
attic/testsuite/helpers.py

@@ -1,5 +1,5 @@
 from datetime import datetime
 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
 from attic.testsuite import AtticTestCase
 
 
 
 
@@ -54,3 +54,15 @@ class PatternTestCase(AtticTestCase):
         self.assert_equal(ExcludePattern('/tmp').match('/tmp'), True)
         self.assert_equal(ExcludePattern('/tmp').match('/tmp'), True)
         self.assert_equal(ExcludePattern('/tmp').match('/tmp/foo'), True)
         self.assert_equal(ExcludePattern('/tmp').match('/tmp/foo'), True)
         self.assert_equal(ExcludePattern('/tmp').match('/tmofoo'), False)
         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('/'), '.')
+