Skip to content

Commit eae17ba

Browse files
aeurielesngpshead
authored andcommitted
00467: tarfile CVE-2025-8194
tarfile now validates archives to ensure member offsets are non-negative (pythonGH-137027) Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent c4d4af3 commit eae17ba

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed

Lib/tarfile.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,6 +1582,9 @@ def _block(self, count):
15821582
"""Round up a byte count by BLOCKSIZE and return it,
15831583
e.g. _block(834) => 1024.
15841584
"""
1585+
# Only non-negative offsets are allowed
1586+
if count < 0:
1587+
raise InvalidHeaderError("invalid offset")
15851588
blocks, remainder = divmod(count, BLOCKSIZE)
15861589
if remainder:
15871590
blocks += 1

Lib/test/test_tarfile.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def md5sum(data):
4343
xzname = os.path.join(TEMPDIR, "testtar.tar.xz")
4444
tmpname = os.path.join(TEMPDIR, "tmp.tar")
4545
dotlessname = os.path.join(TEMPDIR, "testtar")
46+
SPACE = b" "
4647

4748
md5_regtype = "65f477c818ad9e15f7feab0c6d37742f"
4849
md5_sparse = "a54fbc4ca4f4399a90e1b27164012fc6"
@@ -4005,6 +4006,161 @@ def valueerror_filter(tarinfo, path):
40054006
self.expect_exception(TypeError) # errorlevel is not int
40064007

40074008

4009+
class OffsetValidationTests(unittest.TestCase):
4010+
tarname = tmpname
4011+
invalid_posix_header = (
4012+
# name: 100 bytes
4013+
tarfile.NUL * tarfile.LENGTH_NAME
4014+
# mode, space, null terminator: 8 bytes
4015+
+ b"000755" + SPACE + tarfile.NUL
4016+
# uid, space, null terminator: 8 bytes
4017+
+ b"000001" + SPACE + tarfile.NUL
4018+
# gid, space, null terminator: 8 bytes
4019+
+ b"000001" + SPACE + tarfile.NUL
4020+
# size, space: 12 bytes
4021+
+ b"\xff" * 11 + SPACE
4022+
# mtime, space: 12 bytes
4023+
+ tarfile.NUL * 11 + SPACE
4024+
# chksum: 8 bytes
4025+
+ b"0011407" + tarfile.NUL
4026+
# type: 1 byte
4027+
+ tarfile.REGTYPE
4028+
# linkname: 100 bytes
4029+
+ tarfile.NUL * tarfile.LENGTH_LINK
4030+
# magic: 6 bytes, version: 2 bytes
4031+
+ tarfile.POSIX_MAGIC
4032+
# uname: 32 bytes
4033+
+ tarfile.NUL * 32
4034+
# gname: 32 bytes
4035+
+ tarfile.NUL * 32
4036+
# devmajor, space, null terminator: 8 bytes
4037+
+ tarfile.NUL * 6 + SPACE + tarfile.NUL
4038+
# devminor, space, null terminator: 8 bytes
4039+
+ tarfile.NUL * 6 + SPACE + tarfile.NUL
4040+
# prefix: 155 bytes
4041+
+ tarfile.NUL * tarfile.LENGTH_PREFIX
4042+
# padding: 12 bytes
4043+
+ tarfile.NUL * 12
4044+
)
4045+
invalid_gnu_header = (
4046+
# name: 100 bytes
4047+
tarfile.NUL * tarfile.LENGTH_NAME
4048+
# mode, null terminator: 8 bytes
4049+
+ b"0000755" + tarfile.NUL
4050+
# uid, null terminator: 8 bytes
4051+
+ b"0000001" + tarfile.NUL
4052+
# gid, space, null terminator: 8 bytes
4053+
+ b"0000001" + tarfile.NUL
4054+
# size, space: 12 bytes
4055+
+ b"\xff" * 11 + SPACE
4056+
# mtime, space: 12 bytes
4057+
+ tarfile.NUL * 11 + SPACE
4058+
# chksum: 8 bytes
4059+
+ b"0011327" + tarfile.NUL
4060+
# type: 1 byte
4061+
+ tarfile.REGTYPE
4062+
# linkname: 100 bytes
4063+
+ tarfile.NUL * tarfile.LENGTH_LINK
4064+
# magic: 8 bytes
4065+
+ tarfile.GNU_MAGIC
4066+
# uname: 32 bytes
4067+
+ tarfile.NUL * 32
4068+
# gname: 32 bytes
4069+
+ tarfile.NUL * 32
4070+
# devmajor, null terminator: 8 bytes
4071+
+ tarfile.NUL * 8
4072+
# devminor, null terminator: 8 bytes
4073+
+ tarfile.NUL * 8
4074+
# padding: 167 bytes
4075+
+ tarfile.NUL * 167
4076+
)
4077+
invalid_v7_header = (
4078+
# name: 100 bytes
4079+
tarfile.NUL * tarfile.LENGTH_NAME
4080+
# mode, space, null terminator: 8 bytes
4081+
+ b"000755" + SPACE + tarfile.NUL
4082+
# uid, space, null terminator: 8 bytes
4083+
+ b"000001" + SPACE + tarfile.NUL
4084+
# gid, space, null terminator: 8 bytes
4085+
+ b"000001" + SPACE + tarfile.NUL
4086+
# size, space: 12 bytes
4087+
+ b"\xff" * 11 + SPACE
4088+
# mtime, space: 12 bytes
4089+
+ tarfile.NUL * 11 + SPACE
4090+
# chksum: 8 bytes
4091+
+ b"0010070" + tarfile.NUL
4092+
# type: 1 byte
4093+
+ tarfile.REGTYPE
4094+
# linkname: 100 bytes
4095+
+ tarfile.NUL * tarfile.LENGTH_LINK
4096+
# padding: 255 bytes
4097+
+ tarfile.NUL * 255
4098+
)
4099+
valid_gnu_header = tarfile.TarInfo("filename").tobuf(tarfile.GNU_FORMAT)
4100+
data_block = b"\xff" * tarfile.BLOCKSIZE
4101+
4102+
def _write_buffer(self, buffer):
4103+
with open(self.tarname, "wb") as f:
4104+
f.write(buffer)
4105+
4106+
def _get_members(self, ignore_zeros=None):
4107+
with open(self.tarname, "rb") as f:
4108+
with tarfile.open(
4109+
mode="r", fileobj=f, ignore_zeros=ignore_zeros
4110+
) as tar:
4111+
return tar.getmembers()
4112+
4113+
def _assert_raises_read_error_exception(self):
4114+
with self.assertRaisesRegex(
4115+
tarfile.ReadError, "file could not be opened successfully"
4116+
):
4117+
self._get_members()
4118+
4119+
def test_invalid_offset_header_validations(self):
4120+
for tar_format, invalid_header in (
4121+
("posix", self.invalid_posix_header),
4122+
("gnu", self.invalid_gnu_header),
4123+
("v7", self.invalid_v7_header),
4124+
):
4125+
with self.subTest(format=tar_format):
4126+
self._write_buffer(invalid_header)
4127+
self._assert_raises_read_error_exception()
4128+
4129+
def test_early_stop_at_invalid_offset_header(self):
4130+
buffer = self.valid_gnu_header + self.invalid_gnu_header + self.valid_gnu_header
4131+
self._write_buffer(buffer)
4132+
members = self._get_members()
4133+
self.assertEqual(len(members), 1)
4134+
self.assertEqual(members[0].name, "filename")
4135+
self.assertEqual(members[0].offset, 0)
4136+
4137+
def test_ignore_invalid_archive(self):
4138+
# 3 invalid headers with their respective data
4139+
buffer = (self.invalid_gnu_header + self.data_block) * 3
4140+
self._write_buffer(buffer)
4141+
members = self._get_members(ignore_zeros=True)
4142+
self.assertEqual(len(members), 0)
4143+
4144+
def test_ignore_invalid_offset_headers(self):
4145+
for first_block, second_block, expected_offset in (
4146+
(
4147+
(self.valid_gnu_header),
4148+
(self.invalid_gnu_header + self.data_block),
4149+
0,
4150+
),
4151+
(
4152+
(self.invalid_gnu_header + self.data_block),
4153+
(self.valid_gnu_header),
4154+
1024,
4155+
),
4156+
):
4157+
self._write_buffer(first_block + second_block)
4158+
members = self._get_members(ignore_zeros=True)
4159+
self.assertEqual(len(members), 1)
4160+
self.assertEqual(members[0].name, "filename")
4161+
self.assertEqual(members[0].offset, expected_offset)
4162+
4163+
40084164
def setUpModule():
40094165
support.unlink(TEMPDIR)
40104166
os.makedirs(TEMPDIR)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`tarfile` now validates archives to ensure member offsets are
2+
non-negative. (Contributed by Alexander Enrique Urieles Nieto in
3+
:gh:`130577`.)

0 commit comments

Comments
 (0)