You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Pillow did not limit the amount of GZIP-compressed data read when decoding a FITS image, making it 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).
Patches
The amount of data read is now limited to the necessary amount.
Fixed in Pillow 12.2.0 (PR #9521).
Passing nested lists as coordinates to APIs that accept coordinates such as ImagePath.Path, ImageDraw.ImageDraw.polygon and ImageDraw.ImageDraw.line could cause a heap buffer overflow, as nested lists were recursively unpacked beyond the allocated buffer. Coordinate lists are now validated to contain exactly two numeric coordinates. This was introduced in Pillow 11.2.1.
Processing a malicious PSD file could lead to memory corruption, potentially resulting in a crash or arbitrary code execution.
Patches
Patched version: 12.2.0
Pillow 12.1.1 addressed CVE-2026-25990 by adding checks for tile extents in PSD image decoding/encoding to prevent an out-of-bounds write. However, the bounds checks computed tile extent sums using types susceptible to integer overflow, meaning a PSD image with carefully chosen tile dimensions could produce values that wrap around and bypass the checks, still triggering an out-of-bounds write in src/decode.c and src/encode.c. The fix avoids adding extents together before comparison.
Workarounds
Use any version but affected versions: >= 10.3.0, < 12.2.0
If a font advances for each glyph by an exceeding large amount, when Pillow keeps track of the current position, it may lead to an integer overflow. This has been fixed.
An attacker can supply a malicious PDF that causes the process to hang indefinitely, consuming 100% CPU and making the application unresponsive.
Patches
Patched version: 12.2.0.
PdfParser (introduced in Pillow 4.2.0) follows Prev pointers in PDF trailers to read cross-reference sections. If a
trailer's Prev pointer references an offset that has already been processed — either pointing to itself or forming a
longer cycle — the parser enters an infinite loop. Pillow now tracks previously processed trailer offsets and raises an
error if a cycle is detected.
Workarounds
Use any version but the affected versions: >= 4.2.0, < 12.2.0
Pillow's EPS parser (PIL/EpsImagePlugin.py) accepts a negative byte count in the %%BeginBinary directive. A crafted EPS file can cause Image.open() to seek backwards to the same directive and parse it repeatedly, resulting in an infinite loop and CPU denial of service.
The issue is triggered during Image.open(), does not require Image.load(), and does not require Ghostscript execution.
Confirmed affected versions: Pillow 12.0.0 through 12.2.0.
Details
The issue is in the EPS parser in PIL/EpsImagePlugin.py. When parsing an EPS %%BeginBinary directive, Pillow reads the byte count from the file and passes it directly to a relative seek operation without validating that the value is non-negative.
There is no validation that bytecount is non-negative.
If an attacker provides a negative value such as %%BeginBinary:-18, the parser moves the file pointer backwards from the end of the directive line to the same line region. The next parser iteration reads the same %%BeginBinary:-18 directive again, performs the same backward seek, and repeats indefinitely. This causes Image.open() to hang in an infinite loop and consume CPU.
In local testing, the issue is present in Pillow 12.0.0, 12.1.0, 12.1.1, and 12.2.0. Pillow 11.3.0 did not hang with the same PoC, so this appears to affect the 12.x EPS parsing path.
PoC
Save the following content as pillow_eps_beginbinary_dos.eps:
Expected behavior: Pillow should reject the malformed EPS file with a parser exception.
Actual behavior: the process does not return. It hangs inside Image.open() and continuously consumes CPU.
The loop behavior can be observed by tracing the parser state. The file pointer repeatedly seeks from position 112 back to 94, causing the same %%BeginBinary:-18 line to be parsed again and again:
LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94
LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94
LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94
Impact
This is a denial-of-service vulnerability. An attacker who can provide an EPS file to an application using Pillow for image validation, metadata parsing, previews, uploads, or batch image processing can cause the image parsing process to hang during Image.open().
This can impact web services and backend workers that parse untrusted image files, especially if image parsing is performed in a main worker process without CPU limits, timeouts, or process isolation. The issue does not require Ghostscript execution and does not require calling Image.load(), so applications that only use Image.open() to validate or identify uploaded images may still be affected.
Suggested fix: validate the parsed %%BeginBinary byte count before seeking. If the byte count is negative, reject the file with a parsing exception instead of calling self.fp.seek(bytecount, os.SEEK_CUR).
When Pillow loads an uncompressed image whose tile uses the raw codec and a mode in Image._MAPMODES, and the image was opened from a filename, it memory-maps the file and builds the image's row pointers directly into the mapping via PyImaging_MapBuffer (src/map.c). The per-row spacing (stride) is taken from the tile arguments. map.c validates offset + ysize*stride <= buffer_len but never checks that stride is at least the natural row width xsize * pixelsize.
The McIdas AREA plugin (McIdasImagePlugin.py) derives stride, offset, xsize, and ysize directly from attacker-controlled 32-bit header words with no validation. By supplying a stride far smaller than the row width, an attacker makes each row pointer read xsize*pixelsize bytes that run past the mapped region. Accessing the pixels (e.g. Image.tobytes(), getpixel, convert, save) then reads adjacent process memory (information disclosure) or faults (SIGBUS, denial of service).
Complete Code Trace
Step 1: McIdasImageFile._open - turns attacker header words into image size, file offset, and row stride with no validation.
im->linesize (the number of bytes any consumer reads per row) is xsize * pixelsize = 200000, but the row pointers are only stride = 1 byte apart and the buffer is only offset + ysize*stride = 2 bytes "claimed". Nothing reconciles the two.
Step 4: pixel access (Image.tobytes() → raw encoder copy1) - reads linesize bytes from im->image[0], i.e. xsize bytes starting at view.buf + offset, running far past the mmap.
/* the raw "L" packer copies linesize (=xsize) bytes per row from im->image[y]; for row 0 that is view.buf+1 .. view.buf+1+200000, vs a 256-byte file. */
Chain Summary
SOURCE: McIdas AREA header words w[9],w[10],w[11],w[14],w[15],w[34] (Image.open on a path)
↓ McIdasImagePlugin._open: stride = w[15]+w[10]*w[11]*w[14] -> attacker sets stride=1 [McIdasImagePlugin.py:66]
↓ tile = ("raw", (0,0,xsize,1), offset, ("L", 1, 1)) [McIdasImagePlugin.py:68]
GADGET: ImageFile.load mmap branch -- only checks offset+ysize*stride<=len <- BUG: no stride>=linesize check [ImageFile.py:343]
↓ core.map_buffer(map, (xsize,1), "raw", offset, ("L",1,1)) [ImageFile.py:346]
SINK: PyImaging_MapBuffer: im->image[0] = view.buf + offset + 0*stride; linesize=xsize [map.c:134]
↓ Image.tobytes() raw "L" encoder reads linesize (=xsize) bytes from im->image[0]
IMPACT: reads xsize bytes from a tiny mmap -> OOB read of adjacent process memory (leak) or SIGBUS (DoS)
Any application that opens image files supplied by users from a path on disk (the common pattern: save upload to a temp file, then Image.open(path)), has the default plugin set (McIdas is registered by default), and subsequently reads/returns/re-encodes the decoded pixels (thumbnailing, format conversion, serving a preview), is exposed:
Information disclosure (High): the decoded "image" contains bytes of the worker process's adjacent heap/mapped memory, which the app then serves or stores - potentially leaking secrets, credentials, or other users' data.
Denial of service (High): a larger xsize reliably crashes the worker with SIGBUS.
Suggested fix
Core fix in src/map.c (PyImaging_MapBuffer): reject offset < 0 and stride < im->linesize. Defense-in-depth in McIdasImagePlugin._open: reject offset < 0 or stride < xsize*pixelsize .
PIL/FontFile.pyFontFile.compile() assembles per-glyph images into a single combined bitmap using Image.new("1", (xsize, ysize)) without calling Image._decompression_bomb_check(). This is the base-class method shared by both BdfFontFile and PcfFontFile, and it is triggered whenever a loaded font is converted to an ImageFont or saved.
Neither BdfFontFile.BdfFontFile(fp) nor PcfFontFile.PcfFontFile(fp) is registered with Image.register_open(), so Pillow's standard decompression bomb guard never fires for font objects. The compile step is the final opportunity to check the combined allocation — and it has no check.
Vulnerable code (PIL/FontFile.py lines ~64–92):
defcompile(self) ->None:
ifself.bitmap:
returnh=w=maxwidth=0lines=1forglyphinself.glyph: # up to 256 glyph slotsifglyph:
d, dst, src, im=glyphh=max(h, src[3] -src[1]) # max glyph height — attacker-controlledw=w+ (src[2] -src[0])
ifw>WIDTH: # WIDTH = 800lines+=1w=src[2] -src[0]
maxwidth=max(maxwidth, w)
xsize=maxwidth# ≤ 800 (capped by WIDTH constant)ysize=lines*h# ← lines(256) × h(65535) = 16,776,960ifxsize==0andysize==0:
returnself.ysize=h# NO _decompression_bomb_check() here ←self.bitmap=Image.new("1", (xsize, ysize)) # ← unchecked allocation
256 (one per glyph slot, width=800 forces a wrap every glyph)
h (max glyph height)
65,535
xsize
800
ysize = lines × h
256 × 65,535 = 16,776,960
Total pixels
800 × 16,776,960 = 13,421,568,000
Ratio vs. DecompressionBombError threshold
75×
Memory (mode "1", 1 bit/pixel)
~1.6 GB
Steps to reproduce
Proof of Concept script:
#!/usr/bin/env python3"""PoC: FontFile.compile() bomb bypass256 glyphs at 800x875 each (individually below warning threshold)→ compile() creates 800x224000 = 179.2M px bitmap with NO bomb check"""fromPILimportFontFile, ImageMAX_GLYPHS=256GLYPH_W=800GLYPH_H=875# individual: 700K px — below 89.4M warning thresholdclassMockFont(FontFile.FontFile):
def__init__(self):
super().__init__()
# Each glyph is individually safe (700K px < 89.4M warning)im=Image.new("1", (GLYPH_W, GLYPH_H))
foriinrange(MAX_GLYPHS):
self.glyph[i] = (
(GLYPH_W, GLYPH_H),
(0, -GLYPH_H, GLYPH_W, 0),
(0, 0, GLYPH_W, GLYPH_H),
im,
)
##### Confirm bomb check WOULD catch the combined sizecombined_size= (GLYPH_W, MAX_GLYPHS*GLYPH_H)
try:
Image._decompression_bomb_check(combined_size)
print("[FAIL] bomb check did not raise — unexpected")
exceptImage.DecompressionBombErrorase:
print(f"[OK] bomb check WOULD block {combined_size}: {e}")
##### Vulnerable path: compile() has NO bomb checkfont=MockFont()
font.compile() # → Image.new("1", (800, 224000)) — no error raisedpx=font.bitmap.size[0] *font.bitmap.size[1]
threshold=Image.MAX_IMAGE_PIXELS*2print(f"[BYPASS] compile() succeeded: bitmap={font.bitmap.size}")
print(f" pixels={px:,} ({px/threshold:.3f}× DecompressionBombError threshold)")
print(f" No DecompressionBombError raised at any point.")
Expected output:
[OK] bomb check WOULD block (800, 224000): Image size (179200000 pixels) exceeds limit
of 178956970 pixels, could be decompression bomb DOS attack.
[BYPASS] compile() succeeded: bitmap=(800, 224000)
pixels=179,200,000 (1.001× DecompressionBombError threshold)
No DecompressionBombError raised at any point.
Verified live on Pillow 12.2.0 — compile() succeeds with no exception.
Real-world trigger using BDF font file:
fromPILimportBdfFontFileimportio##### Load a crafted BDF font with 256 glyphs each claiming height=65535##### (each glyph individually: 800 × 65535 = 52.4M px — below 89.4M warning)##### compile() combined: 800 × 16,776,960 = 13.4B px — 75× error thresholdfont=BdfFontFile.BdfFontFile(open("crafted_256glyph.bdf", "rb"))
font.to_imagefont() # → compile() → ~1.6 GB allocation, NO bomb check
Attack scenarios:
Scenario
Effect
Web font preview (BdfFontFile(upload).to_imagefont())
DoS with crafted .bdf upload
Server-side font renderer that loads PCF → to_imagefont()
OOM crash
Font pipeline: load → render text
One malicious font file kills the process
Impact
Availability: HIGH — compile() creates a combined bitmap whose pixel count scales as WIDTH × lines × max_glyph_height with no upper bound check. With max PCF glyph height (65,535) and 256 glyphs, the combined allocation is ~1.6 GB. With BDF (text-format, unbounded height), the allocation is limited only by system memory.
Neither BdfFontFile nor PcfFontFile is loaded via Image.open(), so the standard decompression bomb guard is entirely absent from the font loading code path. compile() is the only point where the combined allocation size is known, and it has no check.
Confirmed unpatched on python-pillow/Pillowmain branch as of 2026-06-08.
PIL/PcfFontFile.py_load_bitmaps() (line 227) reads glyph dimensions from the PCF METRICS section and passes them directly to Image.frombytes() without calling Image._decompression_bomb_check(). Dimensions originate from unsigned 16-bit values:
Image.frombytes() calls Image.new() first (allocating the full C-heap buffer), then attempts to fill it. This creates two distinct attack paths:
Persistent attack: Provide matching bitmap data → frombytes() succeeds → image stored in font.glyph[ch] permanently
Transient attack: Provide a 148-byte PCF file with large declared dimensions but no data → Image.new() allocates the full buffer → ValueError → buffer freed → but the spike occurs before Python can respond
PIL/BdfFontFile.pybdf_char() (lines 84–88) reads the BBX width height field from a BDF font file and passes the dimensions directly to Image.new() without calling Image._decompression_bomb_check(). This completely bypasses Pillow's documented decompression bomb protection.
Image.open() enforces MAX_IMAGE_PIXELS = 89,478,485 and raises DecompressionBombError for images exceeding 2 × MAX = 178,956,970 pixels. The BDF font loading path calls Image.new() directly, which only calls _check_size() (validates >= 0) — no pixel count limit.
Vulnerable code (PIL/BdfFontFile.py lines 84–88):
##### width, height from attacker-controlled "BBX width height x y" linetry:
im=Image.frombytes("1", (width, height), bitmap, "hex", "1")
exceptValueError:
# TRIGGERED when BITMAP section is empty (zero hex lines)im=Image.new("1", (width, height)) # ← NO _decompression_bomb_check()!# ^ This image is stored in self.glyph[ch] — persists in memory
Attack trigger: A BDF glyph with BBX 20000 20000 and an empty BITMAP section causes Image.frombytes() to raise ValueError, then Image.new("1", (20000, 20000)) allocates 50 MB of C-heap silently. Image.open() would raise DecompressionBombError for the same dimensions.
PIL/GdImageFile.pyGdImageFile._open() reads image dimensions from the GD 2.x header and stores them in self._size without calling Image._decompression_bomb_check(). Because GdImageFile is not registered with Image.register_open(), it never passes through the standard Image.open() code path that enforces Pillow's decompression bomb guard. The plugin exposes its own entry point — PIL.GdImageFile.open(fp) — which directly instantiates the class, fully bypassing the documented protection.
Vulnerable code (PIL/GdImageFile.py lines 50–61):
def_open(self) ->None:
s=self.fp.read(1037)
ifi16(s) notin [65534, 65535]:
raiseSyntaxError("Not a valid GD 2.x .gd file")
self._mode="P"self._size=i16(s, 2), i16(s, 4) # ← unsigned 16-bit; max 65535 each# NO _decompression_bomb_check() call here ←
...
self.tile= [ImageFile._Tile("raw", (0, 0) +self.size, 1037, "L")]
When load() is subsequently called on the returned image object:
load() → load_prepare() → Image.core.new("P", (65535, 65535))
##### ↑ C-level allocation of 4,294,836,225 bytes ≈ 4.3 GB — no Python bomb check precedes this
Dimension arithmetic:
Field
Value
Maximum width from header
65,535 (unsigned 16-bit)
Maximum height from header
65,535 (unsigned 16-bit)
Maximum pixel count
65,535 × 65,535 = 4,294,836,225
DecompressionBombError threshold
178,956,970 (2 × MAX_IMAGE_PIXELS)
Overshoot ratio
24× above DecompressionBombError threshold
Memory at max dimensions
≈ 4.3 GB (palette-mode: 1 byte/pixel)
Minimum attack file size
1,037 bytes (header only — no pixel data needed)
Comparison with safe sibling plugin (WalImageFile):
WalImageFile is in the same category — not registered with Image.open(), loaded via its own open() helper. It was previously patched with the correct fix:
Availability: HIGH — a single 1,037-byte malicious .gd file causes the host process to attempt a ~4.3 GB C-heap allocation. On systems with insufficient memory this crashes the process. Repeatable — attacker can loop requests to keep the server down.
Confidentiality: None
Integrity: None
Authentication required: No — any public endpoint accepting image uploads is affected
User interaction: None
Any service that calls PIL.GdImageFile.open(user_file) followed by .load() (or any lazy-load trigger) is vulnerable. Because the attack requires only a 1,037-byte file, network bandwidth is not a constraint.
Confirmed unpatched on python-pillow/Pillowmain branch as of 2026-06-08.
WindowsViewer.get_command() constructs a cmd.exe shell command by directly embedding a
file path into an f-string without escaping. The result is passed to subprocess.Popen(..., shell=True). Shell metacharacters in the file path — most
importantly a double-quote (") that breaks out of the wrapping, followed by & — allow
injection of arbitrary cmd.exe commands.
The macOS equivalent (MacViewer) correctly applies shlex.quote() to the same parameter.
The Linux equivalent (UnixViewer) does likewise. Windows is the only platform missing this
protection, despite shlex.quote being already imported on line 21 of ImageShow.py.
Part B — Live execution via os.system() (verified on Windows 11, Pillow 12.1.1):
importos, tempfilefromPIL.ImageShowimportWindowsViewerviewer=WindowsViewer()
poc_dir=tempfile.mkdtemp()
marker=os.path.join(poc_dir, "INJECTION_CONFIRMED.txt")
##### Craft injection: payload writes a marker file (harmless)payload=f'echo REAL_INJECTED > "{marker}"'evil_path=os.path.join(poc_dir, f'poc" & {payload} & echo "')
##### Call the REAL Pillow get_command():real_cmd=viewer.get_command(evil_path)
##### Execute the same way the base Viewer.show_file() does (os.system):os.system(real_cmd)
assertos.path.exists(marker) # PASSES — marker was createdassert"REAL_INJECTED"inopen(marker).read() # PASSES##### → CONFIRMED: arbitrary command injection via get_command()
✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.05%. Comparing base (58f0f1c) to head (9af6788). ⚠️ Report is 1 commits behind head on master.
❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
renovateBot
changed the title
chore(dependencies): update dependency pillow to v12.2.0 [security]
chore(dependencies): update dependency pillow to v12.2.0 [security] - autoclosed
Jun 5, 2026
renovateBot
changed the title
chore(dependencies): update dependency pillow to v12.2.0 [security] - autoclosed
chore(dependencies): update dependency pillow to v12.2.0 [security]
Jun 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
12.1.1→12.3.0FITS GZIP decompression bomb in Pillow
CVE-2026-40192 / GHSA-whj4-6x5x-4v2j
More information
Details
Impact
Pillow did not limit the amount of GZIP-compressed data read when decoding a FITS image, making it 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).
Patches
The amount of data read is now limited to the necessary amount.
Fixed in Pillow 12.2.0 (PR #9521).
Workarounds
Avoid Pillow >= 10.3.0, < 12.2.0
Only open specific image formats, excluding FITS.
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow has a heap buffer overflow with nested list coordinates
CVE-2026-42309 / GHSA-5xmw-vc9v-4wf2
More information
Details
Passing nested lists as coordinates to APIs that accept coordinates such as
ImagePath.Path,ImageDraw.ImageDraw.polygonandImageDraw.ImageDraw.linecould cause a heap buffer overflow, as nested lists were recursively unpacked beyond the allocated buffer. Coordinate lists are now validated to contain exactly two numeric coordinates. This was introduced in Pillow 11.2.1.Severity
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow has an OOB Write with Invalid PSD Tile Extents (Integer Overflow)
CVE-2026-42311 / GHSA-pwv6-vv43-88gr
More information
Details
Impact
Processing a malicious PSD file could lead to memory corruption, potentially resulting in a crash or arbitrary code execution.
Patches
Patched version: 12.2.0
Pillow 12.1.1 addressed CVE-2026-25990 by adding checks for tile extents in PSD image decoding/encoding to prevent an out-of-bounds write. However, the bounds checks computed tile extent sums using types susceptible to integer overflow, meaning a PSD image with carefully chosen tile dimensions could produce values that wrap around and bypass the checks, still triggering an out-of-bounds write in src/decode.c and src/encode.c. The fix avoids adding extents together before comparison.
Workarounds
Use any version but affected versions: >= 10.3.0, < 12.2.0
Resources
Severity
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow has an integer overflow when processing fonts
CVE-2026-42308 / GHSA-wjx4-4jcj-g98j
More information
Details
If a font advances for each glyph by an exceeding large amount, when Pillow keeps track of the current position, it may lead to an integer overflow. This has been fixed.
Severity
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow has a PDF Parsing Trailer Infinite Loop (DoS)
CVE-2026-42310 / GHSA-r73j-pqj5-w3x7
More information
Details
Impact
An attacker can supply a malicious PDF that causes the process to hang indefinitely, consuming 100% CPU and making the application unresponsive.
Patches
Patched version: 12.2.0.
PdfParser (introduced in Pillow 4.2.0) follows Prev pointers in PDF trailers to read cross-reference sections. If a
trailer's Prev pointer references an offset that has already been processed — either pointing to itself or forming a
longer cycle — the parser enters an infinite loop. Pillow now tracks previously processed trailer offsets and raises an
error if a cycle is detected.
Workarounds
Use any version but the affected versions: >= 4.2.0, < 12.2.0
Resources
Severity
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow EpsImagePlugin negative %%BeginBinary byte count causes infinite loop denial of service
CVE-2026-59203 / GHSA-pg7v-jwj7-p798
More information
Details
Summary
Pillow's EPS parser (PIL/EpsImagePlugin.py) accepts a negative byte count in the %%BeginBinary directive. A crafted EPS file can cause Image.open() to seek backwards to the same directive and parse it repeatedly, resulting in an infinite loop and CPU denial of service.
The issue is triggered during Image.open(), does not require Image.load(), and does not require Ghostscript execution.
Confirmed affected versions: Pillow 12.0.0 through 12.2.0.
Details
The issue is in the EPS parser in PIL/EpsImagePlugin.py. When parsing an EPS %%BeginBinary directive, Pillow reads the byte count from the file and passes it directly to a relative seek operation without validating that the value is non-negative.
Relevant code:
There is no validation that bytecount is non-negative.
If an attacker provides a negative value such as %%BeginBinary:-18, the parser moves the file pointer backwards from the end of the directive line to the same line region. The next parser iteration reads the same %%BeginBinary:-18 directive again, performs the same backward seek, and repeats indefinitely. This causes Image.open() to hang in an infinite loop and consume CPU.
In local testing, the issue is present in Pillow 12.0.0, 12.1.0, 12.1.1, and 12.2.0. Pillow 11.3.0 did not hang with the same PoC, so this appears to affect the 12.x EPS parsing path.
PoC
Save the following content as pillow_eps_beginbinary_dos.eps:
Then run:
Expected behavior: Pillow should reject the malformed EPS file with a parser exception.
Actual behavior: the process does not return. It hangs inside Image.open() and continuously consumes CPU.
The loop behavior can be observed by tracing the parser state. The file pointer repeatedly seeks from position 112 back to 94, causing the same %%BeginBinary:-18 line to be parsed again and again:
Impact
This is a denial-of-service vulnerability. An attacker who can provide an EPS file to an application using Pillow for image validation, metadata parsing, previews, uploads, or batch image processing can cause the image parsing process to hang during Image.open().
This can impact web services and backend workers that parse untrusted image files, especially if image parsing is performed in a main worker process without CPU limits, timeouts, or process isolation. The issue does not require Ghostscript execution and does not require calling Image.load(), so applications that only use Image.open() to validate or identify uploaded images may still be affected.
Suggested fix: validate the parsed %%BeginBinary byte count before seeking. If the byte count is negative, reject the file with a parsing exception instead of calling self.fp.seek(bytecount, os.SEEK_CUR).
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow: Out-of-bounds read via attacker-controlled row stride on Pillow's mmap path (McIdas AREA files)
CVE-2026-54058 / GHSA-62p4-gmf7-7g93
More information
Details
Summary
When Pillow loads an uncompressed image whose tile uses the
rawcodec and a mode inImage._MAPMODES, and the image was opened from a filename, it memory-maps the file and builds the image's row pointers directly into the mapping viaPyImaging_MapBuffer(src/map.c). The per-row spacing (stride) is taken from the tile arguments.map.cvalidatesoffset + ysize*stride <= buffer_lenbut never checks thatstrideis at least the natural row widthxsize * pixelsize.The McIdas AREA plugin (
McIdasImagePlugin.py) derivesstride,offset,xsize, andysizedirectly from attacker-controlled 32-bit header words with no validation. By supplying astridefar smaller than the row width, an attacker makes each row pointer readxsize*pixelsizebytes that run past the mapped region. Accessing the pixels (e.g.Image.tobytes(),getpixel,convert,save) then reads adjacent process memory (information disclosure) or faults (SIGBUS, denial of service).Complete Code Trace
Step 1:
McIdasImageFile._open- turns attacker header words into image size, file offset, and row stride with no validation.Step 2:
ImageFile.load(mmap branch) - selects mmap and delegates tomap_buffer.Step 3:
PyImaging_MapBuffer- builds row pointers atstridespacing into the mmap; validates everything exceptstride >= row width.im->linesize(the number of bytes any consumer reads per row) isxsize * pixelsize = 200000, but the row pointers are onlystride = 1byte apart and the buffer is onlyoffset + ysize*stride = 2bytes "claimed". Nothing reconciles the two.Step 4: pixel access (
Image.tobytes()→ raw encodercopy1) - readslinesizebytes fromim->image[0], i.e.xsizebytes starting atview.buf + offset, running far past the mmap.Chain Summary
Proof of Concept
See attached poc.zip
Impact on a Parent Application
Any application that opens image files supplied by users from a path on disk (the common pattern: save upload to a temp file, then
Image.open(path)), has the default plugin set (McIdas is registered by default), and subsequently reads/returns/re-encodes the decoded pixels (thumbnailing, format conversion, serving a preview), is exposed:xsizereliably crashes the worker with SIGBUS.Suggested fix
Core fix in
src/map.c(PyImaging_MapBuffer): rejectoffset < 0andstride < im->linesize. Defense-in-depth inMcIdasImagePlugin._open: rejectoffset < 0orstride < xsize*pixelsize.Severity
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow:
FontFile.compile():Image.new()called without_decompression_bomb_check()CVE-2026-54060 / GHSA-5x94-69rx-g8h2
More information
Details
Description
PIL/FontFile.pyFontFile.compile()assembles per-glyph images into a single combined bitmap usingImage.new("1", (xsize, ysize))without callingImage._decompression_bomb_check(). This is the base-class method shared by bothBdfFontFileandPcfFontFile, and it is triggered whenever a loaded font is converted to anImageFontor saved.Neither
BdfFontFile.BdfFontFile(fp)norPcfFontFile.PcfFontFile(fp)is registered withImage.register_open(), so Pillow's standard decompression bomb guard never fires for font objects. The compile step is the final opportunity to check the combined allocation — and it has no check.Vulnerable code (
PIL/FontFile.pylines ~64–92):"Slow accumulation" attack — per-glyph dimensions stay BELOW warning threshold:
With PCF-maximum glyph height (65,535):
Steps to reproduce
Proof of Concept script:
Expected output:
Verified live on Pillow 12.2.0 — compile() succeeds with no exception.
Real-world trigger using BDF font file:
Attack scenarios:
BdfFontFile(upload).to_imagefont())to_imagefont()Impact
compile()creates a combined bitmap whose pixel count scales asWIDTH × lines × max_glyph_heightwith no upper bound check. With max PCF glyph height (65,535) and 256 glyphs, the combined allocation is ~1.6 GB. With BDF (text-format, unbounded height), the allocation is limited only by system memory.Affected call paths:
BdfFontFile.BdfFontFile(fp).to_imagefont()→FontFile.compile()BdfFontFile.BdfFontFile(fp).save(filename)→FontFile.compile()PcfFontFile.PcfFontFile(fp).to_imagefont()→FontFile.compile()PcfFontFile.PcfFontFile(fp).save(filename)→FontFile.compile()Neither
BdfFontFilenorPcfFontFileis loaded viaImage.open(), so the standard decompression bomb guard is entirely absent from the font loading code path.compile()is the only point where the combined allocation size is known, and it has no check.Confirmed unpatched on
python-pillow/Pillowmainbranch as of 2026-06-08.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow
PcfFontFile._load_bitmaps():Image.frombytes()called without_decompression_bomb_check()— bomb protection bypass via PCF font loadingCVE-2026-54059 / GHSA-8v84-f9pq-wr9x
More information
Details
Description
PIL/PcfFontFile.py_load_bitmaps()(line 227) reads glyph dimensions from the PCFMETRICSsection and passes them directly toImage.frombytes()without callingImage._decompression_bomb_check(). Dimensions originate from unsigned 16-bit values:Maximum exploitable pixel count: 65,535 × 131,070 = 8,589,734,450 pixels — 48× the DecompressionBombError threshold.
Vulnerable code (
PIL/PcfFontFile.pyline 224–227):Image.frombytes()callsImage.new()first (allocating the full C-heap buffer), then attempts to fill it. This creates two distinct attack paths:frombytes()succeeds → image stored infont.glyph[ch]permanentlyImage.new()allocates the full buffer →ValueError→ buffer freed → but the spike occurs before Python can respondSteps to reproduce
Proof of Concept script:
Expected output:
Amplification table:
Impact
PcfFontFile(fp)) is affectedPcfFontFileis never loaded viaImage.open(), so the bomb check protection is completely absent from the entire PCF font loading pathpython-pillow/Pillowmainbranch as of 2026-06-07Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow
BdfFontFile:Image.new()called without_decompression_bomb_check()— bomb protection bypass via font loadingCVE-2026-55379 / GHSA-45hq-cxwh-f6vc
More information
Details
Summary
PIL/BdfFontFile.pybdf_char()(lines 84–88) reads theBBX width heightfield from a BDF font file and passes the dimensions directly toImage.new()without callingImage._decompression_bomb_check(). This completely bypasses Pillow's documented decompression bomb protection.Image.open()enforcesMAX_IMAGE_PIXELS = 89,478,485and raisesDecompressionBombErrorfor images exceeding2 × MAX = 178,956,970pixels. The BDF font loading path callsImage.new()directly, which only calls_check_size()(validates>= 0) — no pixel count limit.Vulnerable code (
PIL/BdfFontFile.pylines 84–88):Attack trigger: A BDF glyph with
BBX 20000 20000and an emptyBITMAPsection causesImage.frombytes()to raiseValueError, thenImage.new("1", (20000, 20000))allocates 50 MB of C-heap silently. Image.open() would raiseDecompressionBombErrorfor the same dimensions.Steps to reproduce
Minimal malicious BDF file (270 bytes):
Proof of Concept script:
Expected output:
Amplified attack (multiple glyphs):
A BDF file defining 256 glyphs each at
BBX 8000 8000causes256 × 7.6 MB = ~1.95 GBtotal C-heap allocation — all silently, bypassing documented bomb protection.Impact
ImageFont.load("user.bdf"),BdfFontFile(fp)) is affectedself.glyph[ch]for the lifetime of the font object — memory is NOT freed until the font is garbage collectedSeverity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow
GdImageFile._open(): image dimensions accepted without_decompression_bomb_check()CVE-2026-55380 / GHSA-phj9-mv4w-65pm
More information
Details
Description
PIL/GdImageFile.pyGdImageFile._open()reads image dimensions from the GD 2.x header and stores them inself._sizewithout callingImage._decompression_bomb_check(). BecauseGdImageFileis not registered withImage.register_open(), it never passes through the standardImage.open()code path that enforces Pillow's decompression bomb guard. The plugin exposes its own entry point —PIL.GdImageFile.open(fp)— which directly instantiates the class, fully bypassing the documented protection.Vulnerable code (
PIL/GdImageFile.pylines 50–61):When
load()is subsequently called on the returned image object:Dimension arithmetic:
DecompressionBombErrorthresholdComparison with safe sibling plugin (
WalImageFile):WalImageFileis in the same category — not registered withImage.open(), loaded via its ownopen()helper. It was previously patched with the correct fix:GdImageFilewas never updated to match, leaving a gap in protection.Steps to reproduce
Proof of Concept script:
Expected output:
Verified live on Pillow 12.2.0.
Two attack paths:
load_prepare()attempts 4.3 GB C allocation →OSErrorafter spikeload()completes, 4.3 GB stays in memory for object lifetimeFor the transient path, a 1,037-byte file is all that is needed. The attacker does not need to upload a large file.
Real-world scenario:
Impact
.gdfile causes the host process to attempt a ~4.3 GB C-heap allocation. On systems with insufficient memory this crashes the process. Repeatable — attacker can loop requests to keep the server down.Any service that calls
PIL.GdImageFile.open(user_file)followed by.load()(or any lazy-load trigger) is vulnerable. Because the attack requires only a 1,037-byte file, network bandwidth is not a constraint.Confirmed unpatched on
python-pillow/Pillowmainbranch as of 2026-06-08.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path
CVE-2026-55798 / GHSA-4x4j-2g7c-83w6
More information
Details
1. Summary
WindowsViewer.get_command()constructs acmd.exeshell command by directly embedding afile path into an f-string without escaping. The result is passed to
subprocess.Popen(..., shell=True). Shell metacharacters in the file path — mostimportantly a double-quote (
") that breaks out of the wrapping, followed by&— allowinjection of arbitrary
cmd.execommands.The macOS equivalent (
MacViewer) correctly appliesshlex.quote()to the same parameter.The Linux equivalent (
UnixViewer) does likewise. Windows is the only platform missing thisprotection, despite
shlex.quotebeing already imported on line 21 ofImageShow.py.2. Vulnerable Code
File:
src/PIL/ImageShow.py, lines 133–150Contrast with macOS — SAFE (line 164–168):
Cross-platform summary:
shlex.quote()?shell=True?MacViewerUnixViewerWindowsViewershlex.quoteis imported on line 21. Its omission from the Windows path is a clearoversight, not a deliberate design choice.
3. Proof of Concept
A full working PoC is at
poc_pillow_injection.py. Key parts:Part A — Injection string construction (static, no execution):
Part B — Live execution via
os.system()(verified on Windows 11, Pillow 12.1.1):Severity
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:LReferences