unit test unlink protection for tarfile
authorPhilipp Gesang <philipp.gesang@intra2net.com>
Mon, 3 Feb 2020 10:03:48 +0000 (11:03 +0100)
committerThomas Jarosch <thomas.jarosch@intra2net.com>
Tue, 4 Feb 2020 13:08:27 +0000 (14:08 +0100)
Add a separate test series that uses Tarfile directly, bypassing
DeltaTar because the latter does not allow fine-grained control
over what mitigations are active.

testing/test_deltatar.py

index ae5e183..4fac62f 100644 (file)
@@ -1822,6 +1822,7 @@ class DeltaTarTest(BaseTest):
             fullpath = os.path.join("source_dir", name)
             assert not os.path.exists(fullpath)
 
+
     def test_restore_malicious_symlinks(self):
         '''
         Creates a full backup containing a symlink and a file of the same name.
@@ -1886,6 +1887,93 @@ class DeltaTarTest(BaseTest):
         assert not os.path.lexists(fullpath)
 
 
+class TarfileTest(BaseTest):
+    pwd = None
+
+    def setUp(self):
+        self.pwd = os.getcwd()
+        os.makedirs("backup_dir", exist_ok=True)
+
+    def tearDown(self):
+        '''
+        Remove temporary files created by unit tests and restore the API
+        functions in *os*.
+        '''
+        os.chdir(self.pwd)
+        shutil.rmtree("backup_dir")
+
+    def test_extract_malicious_symlinks_unlink(self):
+        '''
+        Test symlink mitigation: The destination must be deleted prior to
+        extraction.
+        '''
+        tar_path = os.path.join("backup_dir", "malicious-archive")
+
+        # add symlinks to existing archive
+
+        def add_symlink (a, name, dst):
+            l = tarfile.TarInfo(name)
+            l.type = tarfile.SYMTYPE
+            l.linkname = dst
+            a.addfile(l)
+
+        def add_file (a, name):
+            f = tarfile.TarInfo(name)
+            f.type = tarfile.REGTYPE
+            a.addfile(f)
+
+        # Add a symlink pointing to must-not-exist, then append a file
+        # object at the same path. The file must not end up at
+        # “must-not-exist” (the pointee) but at “not-as-symlink” (the
+        # pointer) that was unlinked prior to extraction.
+        testpath = "test/not-a-symlink"
+        testdst  = "must-not-exist"
+
+        try:
+            with tarfile.open(tar_path, mode="w") as a:
+                add_symlink(a, testpath, testdst)
+                add_file(a, testpath)
+        except tarfile.ReadError as e:
+            if self.MODE == '#' or self.MODE.endswith ("gz"):
+                pass
+            else:
+                raise
+        except ValueError as e:
+            if self.MODE.startswith ('#'):
+                pass # O_APPEND of concat archives not feasible
+            else:
+                raise
+
+        def test_extract(dst, unlink):
+            with tarfile.open(tar_path, mode="r") as a:
+                os.makedirs(dst, exist_ok=True)
+                olddir = os.getcwd()
+                try:
+                    os.chdir(dst)
+                    a.extractall(unlink=unlink)
+                finally:
+                    os.chdir(olddir)
+
+            fullpath = os.path.join(dst, testpath)
+            fulldst  = os.path.join(dst, "test/%s" % testdst)
+
+            if unlink is True:
+                # Check whether the file was extracted. The object at the
+                # symlink location (source) must be the file. The must not
+                # be an object at the symlink destination.
+                assert not os.path.islink(fullpath)
+                assert not os.path.exists(fulldst)
+            else:
+                # Without unlink protection, the file must be found at the
+                # symlink destination with the symlink intact.
+                assert os.path.islink(fullpath)
+                assert os.path.exists(fulldst)
+
+
+        test_extract("test_dst_unlinked" , True)
+        test_extract("test_dst_symlinked", False)
+
+
 def fsapi_access_true (self):
     """
     Chicanery for testing improper use of the *os* module.