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.
- CWE
- CWE-400
- 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_filePayload
__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 ::
