From 5ef55691ca4c3e8e0ea22201b42e257852ae84518b8ec0202351e97f8caa4df2 Mon Sep 17 00:00:00 2001 From: Matej Cepl Date: Fri, 1 Aug 2025 20:20:01 +0000 Subject: [PATCH 1/3] - Add CVE-2025-8194-tarfile-no-neg-offsets.patch which now validates archives to ensure member offsets are non-negative (gh#python/cpython#130577, CVE-2025-8194, bsc#1247249). OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:Factory/python310?expand=0&rev=188 --- CVE-2025-8194-tarfile-no-neg-offsets.patch | 212 +++++++++++++++++++++ python310.changes | 7 + python310.spec | 3 + 3 files changed, 222 insertions(+) create mode 100644 CVE-2025-8194-tarfile-no-neg-offsets.patch diff --git a/CVE-2025-8194-tarfile-no-neg-offsets.patch b/CVE-2025-8194-tarfile-no-neg-offsets.patch new file mode 100644 index 0000000..7d226d6 --- /dev/null +++ b/CVE-2025-8194-tarfile-no-neg-offsets.patch @@ -0,0 +1,212 @@ +From 28d130238bfb5604eef4b594d597f7b5ec951eba Mon Sep 17 00:00:00 2001 +From: Alexander Urieles +Date: Mon, 28 Jul 2025 17:37:26 +0200 +Subject: [PATCH] gh-130577: tarfile now validates archives to ensure member + offsets are non-negative (GH-137027) (cherry picked from commit + 7040aa54f14676938970e10c5f74ea93cd56aa38) + +Co-authored-by: Alexander Urieles +Co-authored-by: Gregory P. Smith +--- + Lib/tarfile.py | 3 + Lib/test/test_tarfile.py | 156 ++++++++++ + Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst | 3 + 3 files changed, 162 insertions(+) + create mode 100644 Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst + +Index: Python-3.10.18/Lib/tarfile.py +=================================================================== +--- Python-3.10.18.orig/Lib/tarfile.py 2025-08-01 22:19:32.977960762 +0200 ++++ Python-3.10.18/Lib/tarfile.py 2025-08-01 22:19:36.731047446 +0200 +@@ -1612,6 +1612,9 @@ + """Round up a byte count by BLOCKSIZE and return it, + e.g. _block(834) => 1024. + """ ++ # Only non-negative offsets are allowed ++ if count < 0: ++ raise InvalidHeaderError("invalid offset") + blocks, remainder = divmod(count, BLOCKSIZE) + if remainder: + blocks += 1 +Index: Python-3.10.18/Lib/test/test_tarfile.py +=================================================================== +--- Python-3.10.18.orig/Lib/test/test_tarfile.py 2025-08-01 22:19:34.277975756 +0200 ++++ Python-3.10.18/Lib/test/test_tarfile.py 2025-08-01 22:19:36.731272825 +0200 +@@ -49,6 +49,7 @@ + xzname = os.path.join(TEMPDIR, "testtar.tar.xz") + tmpname = os.path.join(TEMPDIR, "tmp.tar") + dotlessname = os.path.join(TEMPDIR, "testtar") ++SPACE = b" " + + sha256_regtype = ( + "e09e4bc8b3c9d9177e77256353b36c159f5f040531bbd4b024a8f9b9196c71ce" +@@ -4273,6 +4274,161 @@ + self.expect_exception(TypeError) # errorlevel is not int + + ++class OffsetValidationTests(unittest.TestCase): ++ tarname = tmpname ++ invalid_posix_header = ( ++ # name: 100 bytes ++ tarfile.NUL * tarfile.LENGTH_NAME ++ # mode, space, null terminator: 8 bytes ++ + b"000755" + SPACE + tarfile.NUL ++ # uid, space, null terminator: 8 bytes ++ + b"000001" + SPACE + tarfile.NUL ++ # gid, space, null terminator: 8 bytes ++ + b"000001" + SPACE + tarfile.NUL ++ # size, space: 12 bytes ++ + b"\xff" * 11 + SPACE ++ # mtime, space: 12 bytes ++ + tarfile.NUL * 11 + SPACE ++ # chksum: 8 bytes ++ + b"0011407" + tarfile.NUL ++ # type: 1 byte ++ + tarfile.REGTYPE ++ # linkname: 100 bytes ++ + tarfile.NUL * tarfile.LENGTH_LINK ++ # magic: 6 bytes, version: 2 bytes ++ + tarfile.POSIX_MAGIC ++ # uname: 32 bytes ++ + tarfile.NUL * 32 ++ # gname: 32 bytes ++ + tarfile.NUL * 32 ++ # devmajor, space, null terminator: 8 bytes ++ + tarfile.NUL * 6 + SPACE + tarfile.NUL ++ # devminor, space, null terminator: 8 bytes ++ + tarfile.NUL * 6 + SPACE + tarfile.NUL ++ # prefix: 155 bytes ++ + tarfile.NUL * tarfile.LENGTH_PREFIX ++ # padding: 12 bytes ++ + tarfile.NUL * 12 ++ ) ++ invalid_gnu_header = ( ++ # name: 100 bytes ++ tarfile.NUL * tarfile.LENGTH_NAME ++ # mode, null terminator: 8 bytes ++ + b"0000755" + tarfile.NUL ++ # uid, null terminator: 8 bytes ++ + b"0000001" + tarfile.NUL ++ # gid, space, null terminator: 8 bytes ++ + b"0000001" + tarfile.NUL ++ # size, space: 12 bytes ++ + b"\xff" * 11 + SPACE ++ # mtime, space: 12 bytes ++ + tarfile.NUL * 11 + SPACE ++ # chksum: 8 bytes ++ + b"0011327" + tarfile.NUL ++ # type: 1 byte ++ + tarfile.REGTYPE ++ # linkname: 100 bytes ++ + tarfile.NUL * tarfile.LENGTH_LINK ++ # magic: 8 bytes ++ + tarfile.GNU_MAGIC ++ # uname: 32 bytes ++ + tarfile.NUL * 32 ++ # gname: 32 bytes ++ + tarfile.NUL * 32 ++ # devmajor, null terminator: 8 bytes ++ + tarfile.NUL * 8 ++ # devminor, null terminator: 8 bytes ++ + tarfile.NUL * 8 ++ # padding: 167 bytes ++ + tarfile.NUL * 167 ++ ) ++ invalid_v7_header = ( ++ # name: 100 bytes ++ tarfile.NUL * tarfile.LENGTH_NAME ++ # mode, space, null terminator: 8 bytes ++ + b"000755" + SPACE + tarfile.NUL ++ # uid, space, null terminator: 8 bytes ++ + b"000001" + SPACE + tarfile.NUL ++ # gid, space, null terminator: 8 bytes ++ + b"000001" + SPACE + tarfile.NUL ++ # size, space: 12 bytes ++ + b"\xff" * 11 + SPACE ++ # mtime, space: 12 bytes ++ + tarfile.NUL * 11 + SPACE ++ # chksum: 8 bytes ++ + b"0010070" + tarfile.NUL ++ # type: 1 byte ++ + tarfile.REGTYPE ++ # linkname: 100 bytes ++ + tarfile.NUL * tarfile.LENGTH_LINK ++ # padding: 255 bytes ++ + tarfile.NUL * 255 ++ ) ++ valid_gnu_header = tarfile.TarInfo("filename").tobuf(tarfile.GNU_FORMAT) ++ data_block = b"\xff" * tarfile.BLOCKSIZE ++ ++ def _write_buffer(self, buffer): ++ with open(self.tarname, "wb") as f: ++ f.write(buffer) ++ ++ def _get_members(self, ignore_zeros=None): ++ with open(self.tarname, "rb") as f: ++ with tarfile.open( ++ mode="r", fileobj=f, ignore_zeros=ignore_zeros ++ ) as tar: ++ return tar.getmembers() ++ ++ def _assert_raises_read_error_exception(self): ++ with self.assertRaisesRegex( ++ tarfile.ReadError, "file could not be opened successfully" ++ ): ++ self._get_members() ++ ++ def test_invalid_offset_header_validations(self): ++ for tar_format, invalid_header in ( ++ ("posix", self.invalid_posix_header), ++ ("gnu", self.invalid_gnu_header), ++ ("v7", self.invalid_v7_header), ++ ): ++ with self.subTest(format=tar_format): ++ self._write_buffer(invalid_header) ++ self._assert_raises_read_error_exception() ++ ++ def test_early_stop_at_invalid_offset_header(self): ++ buffer = self.valid_gnu_header + self.invalid_gnu_header + self.valid_gnu_header ++ self._write_buffer(buffer) ++ members = self._get_members() ++ self.assertEqual(len(members), 1) ++ self.assertEqual(members[0].name, "filename") ++ self.assertEqual(members[0].offset, 0) ++ ++ def test_ignore_invalid_archive(self): ++ # 3 invalid headers with their respective data ++ buffer = (self.invalid_gnu_header + self.data_block) * 3 ++ self._write_buffer(buffer) ++ members = self._get_members(ignore_zeros=True) ++ self.assertEqual(len(members), 0) ++ ++ def test_ignore_invalid_offset_headers(self): ++ for first_block, second_block, expected_offset in ( ++ ( ++ (self.valid_gnu_header), ++ (self.invalid_gnu_header + self.data_block), ++ 0, ++ ), ++ ( ++ (self.invalid_gnu_header + self.data_block), ++ (self.valid_gnu_header), ++ 1024, ++ ), ++ ): ++ self._write_buffer(first_block + second_block) ++ members = self._get_members(ignore_zeros=True) ++ self.assertEqual(len(members), 1) ++ self.assertEqual(members[0].name, "filename") ++ self.assertEqual(members[0].offset, expected_offset) ++ ++ + def setUpModule(): + os_helper.unlink(TEMPDIR) + os.makedirs(TEMPDIR) +Index: Python-3.10.18/Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ Python-3.10.18/Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst 2025-08-01 22:19:36.732214922 +0200 +@@ -0,0 +1,3 @@ ++:mod:`tarfile` now validates archives to ensure member offsets are ++non-negative. (Contributed by Alexander Enrique Urieles Nieto in ++:gh:`130577`.) diff --git a/python310.changes b/python310.changes index fae8d74..c787f3e 100644 --- a/python310.changes +++ b/python310.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Fri Aug 1 20:09:24 UTC 2025 - Matej Cepl + +- Add CVE-2025-8194-tarfile-no-neg-offsets.patch which now + validates archives to ensure member offsets are non-negative + (gh#python/cpython#130577, CVE-2025-8194, bsc#1247249). + ------------------------------------------------------------------- Wed Jul 2 14:47:20 UTC 2025 - Matej Cepl diff --git a/python310.spec b/python310.spec index bcfbdf7..01626db 100644 --- a/python310.spec +++ b/python310.spec @@ -205,6 +205,9 @@ Patch28: sphinx-802.patch # PATCH-FIX-UPSTREAM CVE-2025-6069-quad-complex-HTMLParser.patch bsc#1244705 mcepl@suse.com # avoid quadratic complexity when processing malformed inputs with HTMLParser Patch29: CVE-2025-6069-quad-complex-HTMLParser.patch +# PATCH-FIX-UPSTREAM CVE-2025-8194-tarfile-no-neg-offsets.patch bsc#1247249 mcepl@suse.com +# tarfile now validates archives to ensure member offsets are non-negative +Patch30: CVE-2025-8194-tarfile-no-neg-offsets.patch BuildRequires: autoconf-archive BuildRequires: automake BuildRequires: fdupes From 4533735d37dffbfef46376253369382b97287759d4138ec745a3d20e521dfa1c Mon Sep 17 00:00:00 2001 From: Matej Cepl Date: Fri, 1 Aug 2025 20:22:59 +0000 Subject: [PATCH 2/3] update the patch OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:Factory/python310?expand=0&rev=189 --- CVE-2025-8194-tarfile-no-neg-offsets.patch | 54 +++++++++++++++++----- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/CVE-2025-8194-tarfile-no-neg-offsets.patch b/CVE-2025-8194-tarfile-no-neg-offsets.patch index 7d226d6..e4f7f0f 100644 --- a/CVE-2025-8194-tarfile-no-neg-offsets.patch +++ b/CVE-2025-8194-tarfile-no-neg-offsets.patch @@ -1,23 +1,23 @@ -From 28d130238bfb5604eef4b594d597f7b5ec951eba Mon Sep 17 00:00:00 2001 +From 898ac93eeeabfaffbc008dc3201e17cb39c1a957 Mon Sep 17 00:00:00 2001 From: Alexander Urieles Date: Mon, 28 Jul 2025 17:37:26 +0200 -Subject: [PATCH] gh-130577: tarfile now validates archives to ensure member - offsets are non-negative (GH-137027) (cherry picked from commit +Subject: [PATCH] [3.10] gh-130577: tarfile now validates archives to ensure + member offsets are non-negative (GH-137027) (cherry picked from commit 7040aa54f14676938970e10c5f74ea93cd56aa38) Co-authored-by: Alexander Urieles Co-authored-by: Gregory P. Smith --- Lib/tarfile.py | 3 - Lib/test/test_tarfile.py | 156 ++++++++++ + Lib/test/test_tarfile.py | 188 ++++++++++ Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst | 3 - 3 files changed, 162 insertions(+) + 3 files changed, 194 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst Index: Python-3.10.18/Lib/tarfile.py =================================================================== ---- Python-3.10.18.orig/Lib/tarfile.py 2025-08-01 22:19:32.977960762 +0200 -+++ Python-3.10.18/Lib/tarfile.py 2025-08-01 22:19:36.731047446 +0200 +--- Python-3.10.18.orig/Lib/tarfile.py 2025-08-01 22:22:33.661509420 +0200 ++++ Python-3.10.18/Lib/tarfile.py 2025-08-01 22:22:37.753515863 +0200 @@ -1612,6 +1612,9 @@ """Round up a byte count by BLOCKSIZE and return it, e.g. _block(834) => 1024. @@ -30,8 +30,8 @@ Index: Python-3.10.18/Lib/tarfile.py blocks += 1 Index: Python-3.10.18/Lib/test/test_tarfile.py =================================================================== ---- Python-3.10.18.orig/Lib/test/test_tarfile.py 2025-08-01 22:19:34.277975756 +0200 -+++ Python-3.10.18/Lib/test/test_tarfile.py 2025-08-01 22:19:36.731272825 +0200 +--- Python-3.10.18.orig/Lib/test/test_tarfile.py 2025-08-01 22:22:34.991018210 +0200 ++++ Python-3.10.18/Lib/test/test_tarfile.py 2025-08-01 22:22:37.754065449 +0200 @@ -49,6 +49,7 @@ xzname = os.path.join(TEMPDIR, "testtar.tar.xz") tmpname = os.path.join(TEMPDIR, "tmp.tar") @@ -40,10 +40,42 @@ Index: Python-3.10.18/Lib/test/test_tarfile.py sha256_regtype = ( "e09e4bc8b3c9d9177e77256353b36c159f5f040531bbd4b024a8f9b9196c71ce" -@@ -4273,6 +4274,161 @@ +@@ -4273,6 +4274,193 @@ self.expect_exception(TypeError) # errorlevel is not int ++class OverwriteTests(archiver_tests.OverwriteTests, unittest.TestCase): ++ testdir = os.path.join(TEMPDIR, "testoverwrite") ++ ++ @classmethod ++ def setUpClass(cls): ++ p = cls.ar_with_file = os.path.join(TEMPDIR, 'tar-with-file.tar') ++ cls.addClassCleanup(os_helper.unlink, p) ++ with tarfile.open(p, 'w') as tar: ++ t = tarfile.TarInfo('test') ++ t.size = 10 ++ tar.addfile(t, io.BytesIO(b'newcontent')) ++ ++ p = cls.ar_with_dir = os.path.join(TEMPDIR, 'tar-with-dir.tar') ++ cls.addClassCleanup(os_helper.unlink, p) ++ with tarfile.open(p, 'w') as tar: ++ tar.addfile(tar.gettarinfo(os.curdir, 'test')) ++ ++ p = os.path.join(TEMPDIR, 'tar-with-implicit-dir.tar') ++ cls.ar_with_implicit_dir = p ++ cls.addClassCleanup(os_helper.unlink, p) ++ with tarfile.open(p, 'w') as tar: ++ t = tarfile.TarInfo('test/file') ++ t.size = 10 ++ tar.addfile(t, io.BytesIO(b'newcontent')) ++ ++ def open(self, path): ++ return tarfile.open(path, 'r') ++ ++ def extractall(self, ar): ++ ar.extractall(self.testdir, filter='fully_trusted') ++ ++ +class OffsetValidationTests(unittest.TestCase): + tarname = tmpname + invalid_posix_header = ( @@ -205,7 +237,7 @@ Index: Python-3.10.18/Lib/test/test_tarfile.py Index: Python-3.10.18/Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 -+++ Python-3.10.18/Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst 2025-08-01 22:19:36.732214922 +0200 ++++ Python-3.10.18/Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst 2025-08-01 22:22:37.754445878 +0200 @@ -0,0 +1,3 @@ +:mod:`tarfile` now validates archives to ensure member offsets are +non-negative. (Contributed by Alexander Enrique Urieles Nieto in From 956eaa4354e984cec8290bda0542589ac6b34535ffcf824538616994a4791343 Mon Sep 17 00:00:00 2001 From: Matej Cepl Date: Sat, 2 Aug 2025 15:54:24 +0000 Subject: [PATCH 3/3] Upstream patch depended unnecessarily on archiver_tests module, which is not in 3.10.* OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:Factory/python310?expand=0&rev=190 --- CVE-2025-8194-tarfile-no-neg-offsets.patch | 48 ++++------------------ 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/CVE-2025-8194-tarfile-no-neg-offsets.patch b/CVE-2025-8194-tarfile-no-neg-offsets.patch index e4f7f0f..fea1b69 100644 --- a/CVE-2025-8194-tarfile-no-neg-offsets.patch +++ b/CVE-2025-8194-tarfile-no-neg-offsets.patch @@ -9,15 +9,15 @@ Co-authored-by: Alexander Urieles Co-authored-by: Gregory P. Smith --- Lib/tarfile.py | 3 - Lib/test/test_tarfile.py | 188 ++++++++++ + Lib/test/test_tarfile.py | 156 ++++++++++ Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst | 3 - 3 files changed, 194 insertions(+) + 3 files changed, 162 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst Index: Python-3.10.18/Lib/tarfile.py =================================================================== ---- Python-3.10.18.orig/Lib/tarfile.py 2025-08-01 22:22:33.661509420 +0200 -+++ Python-3.10.18/Lib/tarfile.py 2025-08-01 22:22:37.753515863 +0200 +--- Python-3.10.18.orig/Lib/tarfile.py 2025-08-02 17:52:24.521273582 +0200 ++++ Python-3.10.18/Lib/tarfile.py 2025-08-02 17:52:28.444044748 +0200 @@ -1612,6 +1612,9 @@ """Round up a byte count by BLOCKSIZE and return it, e.g. _block(834) => 1024. @@ -30,8 +30,8 @@ Index: Python-3.10.18/Lib/tarfile.py blocks += 1 Index: Python-3.10.18/Lib/test/test_tarfile.py =================================================================== ---- Python-3.10.18.orig/Lib/test/test_tarfile.py 2025-08-01 22:22:34.991018210 +0200 -+++ Python-3.10.18/Lib/test/test_tarfile.py 2025-08-01 22:22:37.754065449 +0200 +--- Python-3.10.18.orig/Lib/test/test_tarfile.py 2025-08-02 17:52:25.849390293 +0200 ++++ Python-3.10.18/Lib/test/test_tarfile.py 2025-08-02 17:52:39.623989523 +0200 @@ -49,6 +49,7 @@ xzname = os.path.join(TEMPDIR, "testtar.tar.xz") tmpname = os.path.join(TEMPDIR, "tmp.tar") @@ -40,42 +40,10 @@ Index: Python-3.10.18/Lib/test/test_tarfile.py sha256_regtype = ( "e09e4bc8b3c9d9177e77256353b36c159f5f040531bbd4b024a8f9b9196c71ce" -@@ -4273,6 +4274,193 @@ +@@ -4273,6 +4274,161 @@ self.expect_exception(TypeError) # errorlevel is not int -+class OverwriteTests(archiver_tests.OverwriteTests, unittest.TestCase): -+ testdir = os.path.join(TEMPDIR, "testoverwrite") -+ -+ @classmethod -+ def setUpClass(cls): -+ p = cls.ar_with_file = os.path.join(TEMPDIR, 'tar-with-file.tar') -+ cls.addClassCleanup(os_helper.unlink, p) -+ with tarfile.open(p, 'w') as tar: -+ t = tarfile.TarInfo('test') -+ t.size = 10 -+ tar.addfile(t, io.BytesIO(b'newcontent')) -+ -+ p = cls.ar_with_dir = os.path.join(TEMPDIR, 'tar-with-dir.tar') -+ cls.addClassCleanup(os_helper.unlink, p) -+ with tarfile.open(p, 'w') as tar: -+ tar.addfile(tar.gettarinfo(os.curdir, 'test')) -+ -+ p = os.path.join(TEMPDIR, 'tar-with-implicit-dir.tar') -+ cls.ar_with_implicit_dir = p -+ cls.addClassCleanup(os_helper.unlink, p) -+ with tarfile.open(p, 'w') as tar: -+ t = tarfile.TarInfo('test/file') -+ t.size = 10 -+ tar.addfile(t, io.BytesIO(b'newcontent')) -+ -+ def open(self, path): -+ return tarfile.open(path, 'r') -+ -+ def extractall(self, ar): -+ ar.extractall(self.testdir, filter='fully_trusted') -+ -+ +class OffsetValidationTests(unittest.TestCase): + tarname = tmpname + invalid_posix_header = ( @@ -237,7 +205,7 @@ Index: Python-3.10.18/Lib/test/test_tarfile.py Index: Python-3.10.18/Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 -+++ Python-3.10.18/Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst 2025-08-01 22:22:37.754445878 +0200 ++++ Python-3.10.18/Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst 2025-08-02 17:52:28.446021271 +0200 @@ -0,0 +1,3 @@ +:mod:`tarfile` now validates archives to ensure member offsets are +non-negative. (Contributed by Alexander Enrique Urieles Nieto in