From: Philipp Gesang Date: Mon, 3 Feb 2020 10:03:48 +0000 (+0100) Subject: unit test unlink protection for tarfile X-Git-Tag: v2.2~4^2 X-Git-Url: http://developer.intra2net.com/git/?p=python-delta-tar;a=commitdiff_plain;h=fcb3615d7501ecdc60d308252981503164f33c59 unit test unlink protection for tarfile 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. --- diff --git a/testing/test_deltatar.py b/testing/test_deltatar.py index ae5e183..4fac62f 100644 --- a/testing/test_deltatar.py +++ b/testing/test_deltatar.py @@ -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.