VAITP Dataset

← Back to the dataset

CVE-2026-40192

Pillow FITS decoder is vulnerable to a DoS via a decompression bomb.

  • CVSS 8.7
  • CWE-400
  • Resource Management
  • Remote

Pillow is a Python imaging library. Versions 10.3.0 through 12.1.1 did not limit the amount of GZIP-compressed data read when decoding a FITS image, making them vulnerable to decompression bomb attacks. A specially crafted FITS file could cause unbounded memory consumption, leading to denial of service (OOM crash or severe performance degradation). If users are unable to immediately upgrade, they should only open specific image formats, excluding FITS, as a workaround.

CVSS base score
8.7
Published
2026-04-15
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Resource Management
Subcategory
Resource Exhaustion
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
Pillow
Fixed by upgrading
Yes

Solution

Upgrade to Pillow version 10.4.0 or later.

Vulnerable code sample

import gzip
import io

class VulnerableFitsImageFile:
    """
    This class conceptually represents the vulnerable FITS plugin from an
    older Pillow version before the decompression bomb fix.
    """
    def __init__(self, fp):
        # In a real plugin, this would read file headers to set up attributes.
        self.fp = fp
        self.mode = "F"
        self.size = (1024, 1024)
        # The 'tile' attribute would describe how the image is stored.
        # Here, we assume it points to a single GZIP-compressed data block.
        self.tile = [("gzip", (0, 0, 1024, 1024), 0, None)]

    def load(self):
        """
        This method is called to decompress and load the actual image data.
        It simulates reading the compressed tile data from the file.
        """
        tile_data = self.fp.read()

        # THE VULNERABLE OPERATION:
        # gzip.decompress is called on the raw tile data from the file without
        # any checks on the output size. A small `tile_data` can expand
        # into a massive memory allocation, causing a denial of service.
        uncompressed_data = gzip.decompress(tile_data)

        # In a real implementation, this data would be processed further.
        # For the vulnerability, the memory allocation is the damaging step.
        return uncompressed_data

# --- Main script to demonstrate triggering the vulnerability ---

# 1. Define the size of the memory bomb to create (e.g., 100MB).
uncompressed_size = 100 * 1024 * 1024
bomb_payload = b'\0' * uncompressed_size

# 2. Create the highly compressed data payload. A large block of null bytes
# compresses to a very small size.
compressed_bomb = gzip.compress(bomb_payload)

# 3. Simulate a malicious FITS file by putting the compressed bomb into an
# in-memory file-like object.
malicious_file_stream = io.BytesIO(compressed_bomb)

# 4. Simulate Pillow's internal process of opening the file.
# This creates an instance of our vulnerable class.
image = VulnerableFitsImageFile(malicious_file_stream)

# 5. Trigger the vulnerability by calling the 'load' method.
# This line will attempt to allocate 100MB of memory, which can
# cause the program to crash or become unresponsive.
image.load()

Patched code sample

import gzip
import io

# In Pillow, this is defined in ImageFile.py
MAX_DECOMPRESSED_BYTES = 256 * 1024 * 1024  # 256 MB limit

class DecompressionBombError(OSError):
    """Exception raised for decompression bomb attacks."""
    pass

class DecompressionBombCheck:
    """
    A file-like wrapper to protect against decompression bomb attacks.
    It tracks the number of bytes read from the underlying stream and
    raises a DecompressionBombError if the limit is exceeded.
    """
    def __init__(self, fp):
        self.fp = fp
        self._bytes_read = 0

    def read(self, size=-1):
        if self._bytes_read > MAX_DECOMPRESSED_BYTES:
            raise DecompressionBombError(
                f"Decompressed data exceeds size limit ({MAX_DECOMPRESSED_BYTES} bytes)"
            )
        
        data = self.fp.read(size)
        self._bytes_read += len(data)

        if self._bytes_read > MAX_DECOMPRESSED_BYTES:
            raise DecompressionBombError(
                f"Decompressed data exceeds size limit ({MAX_DECOMPRESSED_BYTES} bytes)"
            )
        return data
    
    # Delegate other necessary file-like methods
    def seek(self, *args, **kwargs):
        return self.fp.seek(*args, **kwargs)

    def tell(self, *args, **kwargs):
        return self.fp.tell(*args, **kwargs)

    def close(self, *args, **kwargs):
        return self.fp.close(*args, **kwargs)


def _open_fits_with_fix(fp):
    """
    Demonstrates the fix applied to the FITS image plugin's open method.

    The vulnerability was that `gzip.GzipFile` was used directly, allowing
    an unlimited amount of data to be decompressed.

    The fix is to wrap the `gzip.GzipFile` stream with the
    `DecompressionBombCheck` utility, which limits the total amount of
    decompressed data that can be read.
    """
    # Check for GZIP magic number
    if fp.read(3) == b"\x1f\x8b\x08":
        fp.seek(0)
        
        # --- THE FIX IS APPLIED HERE ---
        # The GZIP stream is wrapped in a DecompressionBombCheck instance.
        # This prevents reading an excessive amount of decompressed data.
        gzipped_fp = gzip.GzipFile(fileobj=fp)
        safe_fp = DecompressionBombCheck(gzipped_fp)
        # --- END OF FIX ---
        
        # Subsequent operations would now use `safe_fp` instead of the raw stream
        # For demonstration, we will try to read from it.
        try:
            # This would be where the rest of the image parsing happens
            header = safe_fp.read(2880)  # Read the first FITS header block
            print("Successfully read FITS header from GZIP stream.")
            return header
        except DecompressionBombError as e:
            print(f"Caught a potential decompression bomb: {e}")
            return None
    else:
        fp.seek(0)
        # For non-gzipped files, no wrapping is needed
        print("File is not GZIP-compressed.")
        return fp.read(2880)

if __name__ == '__main__':
    # --- Example usage ---

    # 1. A valid, small, gzipped file (will succeed)
    print("--- 1. Testing a safe, small gzipped file ---")
    header_data = b"SIMPLE  =                    T".ljust(80, b" ") * 36
    compressed_data = gzip.compress(header_data)
    safe_file = io.BytesIO(compressed_data)
    _open_fits_with_fix(safe_file)
    
    print("\n" + "="*40 + "\n")
    
    # 2. A "decompression bomb" (will be caught by the fix)
    # This creates a small (200 bytes) file that decompresses to >256MB
    print("--- 2. Testing a decompression bomb ---")
    
    # We need to simulate a large file without storing it in memory
    class MockGzipBomb(io.BytesIO):
        def read(self, size=-1):
            # Decompress to a massive stream of zeros
            return b'\0' * (MAX_DECOMPRESSED_BYTES + 1)
            
    # Wrap the bomb in a GzipFile to make it a valid gzip stream for the check
    bomb_file = io.BytesIO()
    with gzip.GzipFile(fileobj=bomb_file, mode='wb') as zf:
        # Write a small amount of data that will be decompressed by our mock
        zf.write(b'bomb')
    
    bomb_file.seek(0)
    
    # Now, patch the gzip module to use our mock bomb reader
    original_gzip_file = gzip.GzipFile
    gzip.GzipFile = lambda fileobj: MockGzipBomb()

    # Run the fixed function on what appears to be a small file
    _open_fits_with_fix(bomb_file)

    # Restore original gzip behavior
    gzip.GzipFile = original_gzip_file

Payload

__VAITP_MODEL_REFUSED__

Cite this entry

@misc{vaitp:cve202640192,
  title        = {{Pillow FITS decoder is vulnerable to a DoS via a decompression bomb.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-40192},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-40192/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::