Skip to content

GHSA-3j72-p6qg-6xp7 BIT-pillow-2026-96642 CVE-2026-96642 PYSEC-2026-4341

Pillow: TarIO negative size field causes infinite CPU-spin loop

Medium PyPI
Severity
0.0 / 10
Medium · CVSS 4.0
Exploit chance (EPSS)
0.34%
Chance of exploitation in the next 30 days. Higher than 28.02% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-835 Loop with Unreachable Exit Condition ('Infinite Loop')
Fix
13.0.0
Upgrade pillow to this version or later

Description

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:

size = int(s[124:135], 8)
...
self.fh.seek((size + 511) & (~511), io.SEEK_CUR)

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.

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.

from __future__ import annotations

from pathlib import Path

import pytest

from PIL import TarIO

SOURCE_TAR = "Tests/images/hopper.tar"


@pytest.mark.timeout(3)
def test_negative_size_field_does_not_hang(tmp_path: Path) -> None:
    with open(SOURCE_TAR, "rb") as f:
        data = bytearray(f.read())

    data[124:135] = b"  -00001000"  # octal 1000 = decimal 512, negated

    poisoned_tar = tmp_path / "poisoned.tar"
    poisoned_tar.write_bytes(data)

    with pytest.raises((OSError, ValueError)):
        with TarIO.TarIO(str(poisoned_tar), "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, member_name) directly for this to matter, which some apps do to read an image out of a tar archive.

Affected versions

PyPI

PackageVulnerableFixed in
pillow
< 13.0.0
13.0.0

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

pillow PyPI → 13.0.0

pip
pip install --upgrade "pillow>=13.0.0"
poetry
poetry add "pillow>=13.0.0"
uv
uv add "pillow>=13.0.0"

Severity breakdown

CVSS 4.0 0.0

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

Attack vector
Network
Attack complexity
Low
Attack requirements
None
Privileges required
None
User interaction
Passive
Confidentiality (vulnerable system)
None
Integrity (vulnerable system)
None
Availability (vulnerable system)
High
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None

CVSS 3.1 6.5

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

Weakness

CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop')

The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
PYSEC-2023-175
Pillow versions before v10.0.1 bundled libwebp binaries in wheels that are vulnerable to CVE-2023-5129 (previously CVE-2023-4863).
High Known exploited Also affects pillow
GHSA-j7hp-h8jx-5ppr
libwebp: OOB write in BuildHuffmanTable
High Known exploited Also affects pillow
GHSA-78g4-xqwv-q3g8
Pillow: Bundled libopenjp2 2.5.4 in all wheels since 11.1.0 carries OSV-2025-219 heap-buffer-overflow write reachable from Image.open().load()
High Also affects pillow
GHSA-654x-cwwv-5j9c
Pillow: Arrow export leaks stale heap memory when PILLOW_ALIGNMENT > 1
Medium Also affects pillow
GHSA-xvh5-g6gv-559v
Pillow: Memory disclosure and segfault in libtiff encoder via crafted TIFF tags
High Also affects pillow
GHSA-g7cr-3p24-7wf7
Pillow: Heap buffer overflow writing JPEG comment after ICC profile fills buffer
High Also affects pillow
GHSA-9v9v-9qvf-h2hj
Pillow: Heap over-read in WMF/EMF rendering via 16-bit dimension truncation (Windows builds)
High Also affects pillow
GHSA-g4qv-2jxx-769h
Pillow: PNG eXIf chunk makes EXIF parsing run in quadratic time
Medium Also affects pillow
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor