GHSA-3j72-p6qg-6xp7: Pip/Pillow vulnerability
Summary TarIO.init does not check that a tar header's size field is positive. A crafted header with a negative size makes it seek backward onto itself. This causes an infinite loop that uses 100% CPU and never returns.
Details TarIO.init reads 512-byte tar headers one at a time, looking for a member with a given name. For each header it reads the size field and skips ahead to the next header:
python size = int(s[124:135], 8) ... self.fh.seek((size + 511) & (~511), io.SEEKCUR) There is no check that size is zero or more. A tar header can set this field to a negative octal value. For size = -512, the seek offset comes out to exactly -512, which cancels the 512 bytes the loop had just read. The file position goes right back to where this loop step started.
The loop only stops on end of file or when a header name starts with a null byte. Neither happens, since the same header keeps showing up again. The loop never ends on its own.
Steps to reproduce
This uses Tests/images/hopper.tar, a normal file already in the Pillow test suite. It only changes the size field of the first header.
python from PIL import TarIO
with open("Tests/images/hopper.tar", "rb") as f: data = bytearray(f.read())
Set the size field (bytes 124 to 135) to a negative octal value. Octal 1000 is 512, so this is size = -512. data[124:135] = b" -00001000"
with open("poisoned.tar", "wb") as f: f.write(data)
"does-not-exist" does not match the header name, so TarIO must keep scanning. This call never returns. TarIO.TarIO("poisoned.tar", "does-not-exist")
We confirmed this with ps. The process sits at 98 to 100% CPU and never exits on its own. It only stops if killed from outside.
A pytest version of this, using pytest-timeout so the test does not hang forever, is below. On the current code it fails with Failed: Timeout (>3.0s) from pytest-timeout.
python from future import annotations
from pathlib import Path
import pytest
from PIL import TarIO
SOURCETAR = "Tests/images/hopper.tar"
@pytest.mark.timeout(3) def testnegativesizefielddoesnothang(tmppath: Path) -> None: with open(SOURCETAR, "rb") as f: data = bytearray(f.read())
data[124:135] = b" -00001000" # octal 1000 = decimal 512, negated
poisonedtar = tmppath / "poisoned.tar" poisonedtar.writebytes(data)
with pytest.raises((OSError, ValueError)): with TarIO.TarIO(str(poisonedtar), "does-not-exist"): pass Impact
- Availability: an attacker who can get a crafted tar file passed to TarIO.TarIO() can hang the process at full CPU with a file just over 512 bytes. - Confidentiality: none. - Integrity: none. - TarIO is a small helper class. It is not used by Image.open() and is not on the default path most Pillow users hit. An app has to call PIL.TarIO.TarIO(path, membername) directly for this to matter, which some apps do to read an image out of a tar archive.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/Pillowto a version that resolves this vulnerability.Fixed in 13.0.0
Event History
Frequently Asked Questions
What conditions are required to trigger the denial of service?
An attacker needs to supply a crafted tar archive containing a header with a negative size field, and the application must use Pillow's TarIO functionality to search that archive for a member. The malformed size can cause TarIO.__init__ to repeatedly reread the same header.
What is the operational impact if exploitation succeeds?
The affected operation enters an infinite loop, consumes 100% CPU, and does not return on its own. This can deny service to the process handling the crafted archive.
How can I determine whether an archive is attempting to exploit this issue?
Inspect tar headers for negative octal values in the size field. A size of -512 is specifically described as causing the read offset to move back by one 512-byte header block, resulting in the loop.