chore(deps): update dependency pillow to v12.3.0 [security] #198
No reviewers
Labels
No labels
bug
cog
cog
Aurora
cog
Backup
cog
Bible
cog
EmojiInfo
cog
HotReload
cog
IssueCards
cog
Nerdify
cog
Pterodactyl
cog
RobloxVerify
cog
SeaUtils
cog
SentinelCore
cog
TickChanger
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
cswimr/SeaCogs!198
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "renovate/pypi-pillow-vulnerability"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This PR contains the following updates:
12.2.0→12.3.0Pillow
BdfFontFile:Image.new()called without_decompression_bomb_check()— bomb protection bypass via font loadingBIT-pillow-2026-55379 / CVE-2026-55379 / GHSA-45hq-cxwh-f6vc / PYSEC-2026-2255
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 OSV and the GitHub Advisory Database (CC-BY 4.0).
Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path
BIT-pillow-2026-55798 / CVE-2026-55798 / GHSA-4x4j-2g7c-83w6 / PYSEC-2026-2257
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
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Pillow:
FontFile.compile():Image.new()called without_decompression_bomb_check()BIT-pillow-2026-54060 / CVE-2026-54060 / GHSA-5x94-69rx-g8h2 / PYSEC-2026-2254
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 OSV and 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)
BIT-pillow-2026-54058 / CVE-2026-54058 / GHSA-62p4-gmf7-7g93 / PYSEC-2026-3493
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 OSV and the GitHub Advisory Database (CC-BY 4.0).
Pillow: Heap out-of-bounds write
Image.paste()/Image.crop()via signed coordinate overflowBIT-pillow-2026-59199 / CVE-2026-59199 / GHSA-6r8x-57c9-28j4 / PYSEC-2026-3451
More information
Details
Summary
Pillow's public image coordinate APIs can trigger a native heap out-of-bounds
write when given coordinates near the signed 32-bit integer limits. In 4-byte
pixel modes such as
RGBA, this becomes a controlled backward heap underwrite:for a source image of width
W, Pillow writes4 * Wattacker-controlled bytesstarting
4 * Wbytes before the destination row pointer. With successful largeimage allocation, the theoretical upper bound is ~2 GiB backwards from
the destination row.
Minimal public API trigger:
The same root cause is also reachable through
Image.crop()andImage.alpha_composite(). No private API, ctypes, custom Python object, ormalformed image file is needed.
This has been confirmed as an ASAN heap-buffer-overflow write. On normal
non-ASAN Pillow builds, the minimal trigger corrupts the heap and aborts with
double free or corruption (out)Details
src/PIL/Image.py:paste()accepts a 4-tuple box and passes it to the nativeImagingCore.paste()method:src/_imaging.c:_paste()parses the four Python coordinates into signedintvalues and calls
ImagingPaste():src/libImaging/Paste.c:ImagingPaste()computes and clips the region usingsigned
intarithmetic:With
dx0 = 2147483646anddx1 = -2147483648,dx1 - dx0wraps to2.That matches the 2-pixel source image, so the size check passes. The later
dx0 + xsizeclip check wraps around and does not reject the out-of-boundsdestination.
For 4-byte pixel modes such as
RGBA, the paste loop then multipliesdxbypixelsize:For the minimal PoC, this writes 8 attacker-controlled bytes 8 bytes before the
destination row allocation.
The primitive scales with the attacker-controlled source width:
Examples for
RGBA:Pillow's image creation guard currently limits
xsizeto roughlyINT_MAX / 4 - 1, so the theoretical upper bound for thisRGBAunderwrite is2,147,483,640bytes before the destination row pointer. In practice, theusable range depends on memory availability, allocator layout, and process heap
state.
Two other documented APIs reach the same sink:
Image.crop()keepsright - leftsmall, so the Python decompression-bombcheck allows it.
src/libImaging/Crop.cthen computes wrapped pastecoordinates and calls
ImagingPaste().PoC
The following standalone script exercises all three public API paths. Save it
as
b021_poc.pyand run it withpaste,crop, oralpha.Run against an ASAN build:
Observed ASAN signature for the direct
Image.paste()path:On non-ASAN Pillow
12.2.0and local12.3.0.dev0, the direct minimalImage.paste()trigger returns frompaste()and then the process abortsduring cleanup with:
Observed ASAN signature for the
Image.crop()andImage.alpha_composite()paths:
Suggested fix
Avoid signed overflow in paste/crop coordinate arithmetic. Use checked
arithmetic or a wider type before calculating widths and clipped endpoints.
For example, reject boxes whose endpoint subtraction cannot be represented
cleanly, and clip using non-overflowing comparisons:
ImagingCrop()should receive the same treatment forsx1 - sx0,dx0 = -sx0, anddx1 = imIn->xsize - sx0.Impact
This is a heap out-of-bounds write in Pillow's native C extension, reachable
through documented public image APIs.
Applications are impacted if an untrusted user can control image operation
coordinates passed to Pillow, for example crop boxes, paste boxes, or overlay
positions. The bytes written in the direct
Image.paste()variant are copiedfrom the source image, so attacker-controlled source pixels can influence the
out-of-bounds write. For
RGBA, the write is a backward heap underwrite whoseoffset and length are both
4 * source_width, bounded in practice by successfulimage allocation and heap layout.
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 OSV and 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 loadingBIT-pillow-2026-54059 / CVE-2026-54059 / GHSA-8v84-f9pq-wr9x / PYSEC-2026-2253
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 OSV and the GitHub Advisory Database (CC-BY 4.0).
Pillow: Controlled heap out-of-bounds write in Pillow
ImageCmsTransform.apply()via output mode mismatchBIT-pillow-2026-59205 / CVE-2026-59205 / GHSA-9hw9-ch79-4vh6 / PYSEC-2026-3453
More information
Details
Summary
Pillow's public
ImageCms.ImageCmsTransform.apply(im, imOut)API can triggercontrolled native heap corruption when the caller supplies an output image whose
mode does not match the transform's declared output mode.
For example, a transform built as
RGBA -> RGBAcan be applied to anLoutputimage. Pillow checks dimensions only, then calls LittleCMS with the output row
pointer. LittleCMS writes RGBA-sized rows into a 1-byte-per-pixel
Limage row.Details
src/PIL/ImageCms.py:ImageCmsTransform.apply()accepts an optional callersupplied
imOut:If
imOutis provided, Pillow does not check:The C wrapper in
src/_imagingcms.cunwraps both image cores and only checksthat the output dimensions are at least as large as the input dimensions:
findLCMStype()mapsRGB,RGBA, andRGBXtransform modes to LittleCMSTYPE_RGBA_8, which writes 4 bytes per pixel:So with a transform declared as
RGBA -> RGBA, LittleCMS writes4 * widthbytes to each output row. If the supplied output image is mode
L, Pillow onlyallocated
1 * widthbytes for that row.For width 4096:
The bug does not require a large image. Width 8 was enough to corrupt heap
metadata. At width 8,
apply()returned to Python and printedafter; glibcdetected the corrupted heap later during cleanup.
PoC
Tiny heap corruption trigger:
Observed locally on Pillow
12.3.0.dev0:Controlled overwrite evidence PoC:
Run under gdb:
Observed on Pillow
12.3.0.dev0:0x4443424144434241is the attacker-controlled source pixel patternb"ABCDABCD"interpreted as a little-endian pointer-sized value.Using source pixels
(1, 2, 3, 4)similarly produced a faulting pointer of0x403020104030201, matching the repeated pixel bytes.Impact
This is a heap out-of-bounds write in Pillow's native ImageCms extension,
reachable through public API.
Applications are impacted if untrusted users can control ImageCms transform
parameters and/or provide the output image object passed to
ImageCmsTransform.apply(). The source image pixels influence the bytes writtenout of bounds.
Suggested fix
Validate modes before calling into the native transform:
The C extension should also defensively reject mismatched image modes before
calling
cmsDoTransform().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 OSV and the GitHub Advisory Database (CC-BY 4.0).
Pillow TGA RLE encoder can serialize up to ~57 KB of adjacent heap data into generated images
BIT-pillow-2026-59198 / CVE-2026-59198 / GHSA-fj7v-r99m-22gq / PYSEC-2026-3494
More information
Details
Summary
Pillow's TGA RLE encoder reads past its row buffer when saving a mode
"1"image. Adjacent process heap bytes can be copied into the generated TGA file.
The bug is reachable through the public save API:
Older affected Pillow versions use the equivalent public option
rle=True.For mode
"1", Pillow allocates a packed row buffer ofceil(width / 8)bytes, but
ImagingTgaRleEncode()treats the row as one full byte per pixel.The maximum valid TGA width is
65535. At that width:On non-ASAN Pillow
12.2.0, the public-only maximum-width PoC below serialized57297bytes from distinct out-of-bounds source offsets into one returned TGA,covering
99.92%of the maximum adjacent heap window. No heap grooming, ctypes,private API, or malformed input file was used. The disclosure is emitted across
many TGA packet payload copies of at most
128bytes each, not one largememcpy().Details
src/PIL/TgaImagePlugin.pyallows mode"1"TGA output and selects thetga_rleencoder when RLE compression is requested.src/encode.c:_setimage()allocates the row buffer using the packed-bitformula:
For mode
"1",state->bits == 1.src/libImaging/TgaRleEncode.cthen computes:This becomes
1, and the encoder uses pixel indexes as byte offsets:The packet payload
memcpy()later copies those out-of-bounds source bytes intothe output. Raw packets copy up to
128contiguous bytes, while RLE packets copyone representative byte:
A width-2 mode
"1"image allocates one row byte and already triggers an ASANheap-buffer-overflow read. Wider images increase the adjacent heap window and
the amount of heap data that can be serialized.
PoC
Minimal ASAN trigger
Observed on local Pillow
12.3.0.dev0ASAN target:Maximum-width heap disclosure
This PoC uses one maximum-width row. It parses the generated TGA packets and
extracts only payload bytes whose source offsets were outside the allocated
packed row. Rows are avoided because they mostly repeat the same adjacent heap window.
Run the following with a standard affected Pillow installation.
Observed on installed Pillow
12.2.0:Impact
This is a heap out-of-bounds read and potential information disclosure.
A maximum-width single-row image can cause nearly the full
57343-byte adjacent heap window to be incorporated into one output file.Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:LReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Pillow: Decompression Bomb DoS via PdfParser.PdfStream.decode()
BIT-pillow-2026-59200 / CVE-2026-59200 / GHSA-jjj6-mw9f-p565 / PYSEC-2026-3495
More information
Details
Summary
PdfParser.PdfStream.decode()in Pillow'sPdfParser.pycallszlib.decompress()with thebufsizeparameter set to the value of the PDF stream'sLengthfield, without any upper bound on the actual decompressed output size. Python'szlib.decompress()bufsizeargument is an initial output buffer hint, not a maximum size limit — the function will expand memory until the full decompressed result is produced. A crafted PDF containing a FlateDecode-compressed stream decompresses to 1 GB of memory from a ~950 KB file, causing server OOM termination or severe degradation in any application that usesPdfParserto read untrusted PDF files.Details
PdfStream.decode()inpdfminer/PdfParser.pyreads the stream's declaredLength(orDL) field from the PDF dictionary and passes it asbufsizetozlib.decompress():From the Python documentation: "The
bufsizeparameter is used as the initial size of the output buffer." It does not cap decompression. An attacker who controls the PDF stream contents can provide a highly-compressed payload that expands to gigabytes, while settingLengthto any value (including the actual compressed size) to avoid triggering format validation.PdfParseris instantiated with a filename or file object and callsread_pdf_info()on open, which parses the xref table and makes stream objects accessible.PdfStream.decode()is reachable whenever calling code accesses a compressed stream object from the parsed PDF.Confirmed reachable path:
PoC
Actual output (Pillow 12.1.1, Python 3.12):
Measured expansion:
Impact
This is a denial-of-service vulnerability. Any application that uses
PIL.PdfParser.PdfParserto read untrusted PDF files is affected. An unauthenticated attacker who can submit a PDF for processing can exhaust all available server memory with a ~950 KB file, causing OOM termination or service degradation affecting all concurrent users. No authentication or user interaction beyond submitting the file is required.Note: This vulnerability is independent of CVE-2025-64512 / CVE-2025-70559 (pdfminer.six) and the companion
PIL/PdfImagePlugin.pydecompression issue. It exists specifically in Pillow's ownPdfParser.pymodule, which is distinct from pdfminer.six.Suggested fix:
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 OSV and the GitHub Advisory Database (CC-BY 4.0).
Pillow EpsImagePlugin negative %%BeginBinary byte count causes infinite loop denial of service
BIT-pillow-2026-59203 / CVE-2026-59203 / GHSA-pg7v-jwj7-p798 / PYSEC-2026-3452
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 OSV and the GitHub Advisory Database (CC-BY 4.0).
Pillow
GdImageFile._open(): image dimensions accepted without_decompression_bomb_check()BIT-pillow-2026-55380 / CVE-2026-55380 / GHSA-phj9-mv4w-65pm / PYSEC-2026-2256
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 OSV and the GitHub Advisory Database (CC-BY 4.0).
Pillow JPEG2000 tiled decode retains a growing scratch buffer and can be used for denial of service
BIT-pillow-2026-59204 / CVE-2026-59204 / GHSA-vjc4-5qp5-m44j / PYSEC-2026-3496
More information
Details
Summary
src/libImaging/Jpeg2KDecode.c:853accumulatestotal_component_widthacross every tile in a JPEG2000 image instead of recomputing it per tile. That accumulated value is then used in thetile_bytescalculation atsrc/libImaging/Jpeg2KDecode.c:868, which can make the decoder growstate->bufferviareallocatsrc/libImaging/Jpeg2KDecode.c:876up to roughly one full image's decompressed size even when each tile is small. A crafted tiled JPEG2000 file can therefore force substantially higher transient memory usage and trigger out-of-memory failures during decoding. Based on current evidence, the supported impact is denial of service, not memory corruption.Details
src/libImaging/Jpeg2KDecode.c:853total_component_widthis initialized only once before the tile loop and keeps growing across tiles. It is then used to derivetile_bytes, so later tiles are treated as if they had the combined component width of all earlier tiles.tile_bytesis promoted intotile_info.data_size, thenstate->bufferis grown withreallocatsrc/libImaging/Jpeg2KDecode.c:876.Image.open(...).load()decoding.PoC
The attached helper script and testcase were used:
exercise_j2k_tile_realloc.zip
Generate the testcase:
Expected geometry from the helper:
3664 x 3664RGBA1832 x 1832(2x2tiles)image_bytes=53699584maxrss_kb=180264maxrss_kb=138404Load it with the current vulnerable build:
Load it again under a 160 MB address-space cap:
Impact
Conservative impact: denial of service through memory exhaustion during JPEG2000 decoding.
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 OSV and the GitHub Advisory Database (CC-BY 4.0).
Pillow: Heap out-of-bounds write in
ImageFilter.RankFiltervia integer overflow inImagingExpandBIT-pillow-2026-59197 / CVE-2026-59197 / GHSA-xj96-63gp-2gmr / PYSEC-2026-3454
More information
Details
Summary
Pillow's public rank-filter API can trigger a native heap out-of-bounds write
when given a very large odd filter size.
Minimal public API trigger:
ImageFilter.RankFilter.filter()callsimage.expand(size // 2, size // 2)before rank-filter size validation. With
size = 4294967295, theexpansion margin is
2147483647(INT_MAX).ImagingExpand()then computesthe output dimensions with unchecked signed
intarithmetic. On tested builds,this wraps to a tiny output image and the border-expansion loop writes past the
allocation.
This is reachable through documented public classes (
RankFilter,MedianFilter,MinFilter, andMaxFilter). No private API, ctypes, or customPython object is needed.
Details
Current
src/PIL/ImageFilter.py:The
expand()call is made beforeimage.rankfilter(...).Current
src/libImaging/Filter.c:ImagingExpand()does not check output-sizeoverflow:
For a
3x3image andxmargin = ymargin = INT_MAX, the computed output sizewraps to
1x1on tested builds. The following loop still uses the huge margin:src/libImaging/RankFilter.cdoes contain checks that would reject this size:But those checks are reached only after
RankFilter.filter()has alreadycalled
image.expand(...).Mode
"L"produces 1-byte OOB stores. Modes"I"and"F"produce 4-byte OOBstores. The repeated value written OOB is copied from the source image border
pixel, so attacker-supplied image bytes can influence it. This is a sequential
overwrite, not an arbitrary-address write.
PoC
Minimal ASAN crash PoC:
Observed on local Pillow
12.3.0.dev0ASAN target:4-byte write variant with source pixel loaded from normal image bytes:
Observed ASAN signature:
Version checks:
Impact
It is a heap out-of-bounds write in Pillow's native C extension, reachable
through public image-filter classes.
Applications are impacted if an untrusted user can control the rank-filter
size/configuration passed to Pillow. If the image is also attacker-supplied, the
source pixel value written out of bounds can be attacker-influenced, including
4-byte values for mode
"I"images.Possible fix
Validate the rank-filter size before calling
image.expand(...), and hardenImagingExpand()against invalid margins and overflow:Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
BIT-pillow-2026-54059 / CVE-2026-54059 / GHSA-8v84-f9pq-wr9x / PYSEC-2026-2253
More information
Details
Pillow is a Python imaging library. Prior to 12.3.0, PIL/PcfFontFile.py _load_bitmaps() read glyph dimensions from the PCF METRICS section and passed them directly to Image.frombytes() without calling Image._decompression_bomb_check(), allowing crafted PCF font data to cause excessive memory allocation. This issue is fixed in version 12.3.0.
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 OSV and the PyPI Advisory Database (CC-BY 4.0).
BIT-pillow-2026-54060 / CVE-2026-54060 / GHSA-5x94-69rx-g8h2 / PYSEC-2026-2254
More information
Details
Pillow is a Python imaging library. Prior to 12.3.0, PIL/FontFile.py FontFile.compile() assembled per-glyph images into a combined bitmap with Image.new("1", (xsize, ysize)) without calling Image._decompression_bomb_check(), allowing a font to trigger excessive allocation during conversion or saving. This issue is fixed in version 12.3.0.
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 OSV and the PyPI Advisory Database (CC-BY 4.0).
BIT-pillow-2026-55379 / CVE-2026-55379 / GHSA-45hq-cxwh-f6vc / PYSEC-2026-2255
More information
Details
Pillow is a Python imaging library. Prior to 12.3.0, PIL/BdfFontFile.py bdf_char() read the BBX width and height field from a BDF font file and passed attacker-controlled dimensions to Image.new() without calling Image._decompression_bomb_check(), bypassing Pillow's documented decompression bomb protection and allowing excessive memory allocation. This issue is fixed in version 12.3.0.
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 OSV and the PyPI Advisory Database (CC-BY 4.0).
BIT-pillow-2026-55380 / CVE-2026-55380 / GHSA-phj9-mv4w-65pm / PYSEC-2026-2256
More information
Details
Pillow is a Python imaging library. Prior to 12.3.0, PIL/GdImageFile.py GdImageFile._open() read image dimensions from the GD 2.x header and stored them in self._size without calling Image._decompression_bomb_check(), allowing a crafted .gd file to trigger excessive C-heap allocation when loaded. This issue is fixed in version 12.3.0.
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 OSV and the PyPI Advisory Database (CC-BY 4.0).
BIT-pillow-2026-55798 / CVE-2026-55798 / GHSA-4x4j-2g7c-83w6 / PYSEC-2026-2257
More information
Details
Pillow is a Python imaging library. Prior to 12.3.0, WindowsViewer.get_command() constructed a cmd.exe shell command by directly embedding a file path into an f-string without escaping and passed the result to subprocess.Popen(..., shell=True), allowing shell metacharacters in the file path to inject arbitrary cmd.exe commands. This issue is fixed in version 12.3.0.
Severity
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:LReferences
This data is provided by OSV and the PyPI Advisory Database (CC-BY 4.0).
BIT-pillow-2026-59199 / CVE-2026-59199 / GHSA-6r8x-57c9-28j4 / PYSEC-2026-3451
More information
Details
Pillow is a Python imaging library. Prior to 12.3.0, Pillow public image coordinate APIs can trigger a native heap out-of-bounds write when given coordinates near the signed 32-bit integer limits in Image.paste(), Image.crop(), or Image.alpha_composite(). This issue is fixed in version 12.3.0.
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 OSV and the PyPI Advisory Database (CC-BY 4.0).
BIT-pillow-2026-59203 / CVE-2026-59203 / GHSA-pg7v-jwj7-p798 / PYSEC-2026-3452
More information
Details
Pillow is a Python imaging library. From 12.0.0 through 12.2.0, Pillow's EPS parser in PIL/EpsImagePlugin.py accepts a negative byte count in the %%BeginBinary directive, allowing a crafted EPS file to cause Image.open() to seek backwards to the same directive and parse it repeatedly in an infinite loop. This issue is fixed in version 12.3.0.
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 OSV and the PyPI Advisory Database (CC-BY 4.0).
BIT-pillow-2026-59205 / CVE-2026-59205 / GHSA-9hw9-ch79-4vh6 / PYSEC-2026-3453
More information
Details
Pillow is a Python imaging library. Prior to 12.3.0, Pillow's ImageCms.ImageCmsTransform.apply(im, imOut) API can trigger controlled native heap corruption when the caller supplies an output image whose mode does not match the transform's declared output mode. This issue is fixed in version 12.3.0.
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 OSV and the PyPI Advisory Database (CC-BY 4.0).
BIT-pillow-2026-59197 / CVE-2026-59197 / GHSA-xj96-63gp-2gmr / PYSEC-2026-3454
More information
Details
Pillow is a Python imaging library. Prior to 12.3.0, Pillow's public rank-filter API can trigger a native heap out-of-bounds write when given a very large odd filter size because ImageFilter.RankFilter.filter() calls image.expand(size // 2, size // 2) before rank-filter size validation and ImagingExpand() computes output dimensions with unchecked signed int arithmetic. This issue is fixed in version 12.3.0.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:HReferences
This data is provided by OSV and the PyPI Advisory Database (CC-BY 4.0).
Pillow: Out-of-bounds read via attacker-controlled row stride on Pillow's mmap path (McIdas AREA files)
BIT-pillow-2026-54058 / CVE-2026-54058 / GHSA-62p4-gmf7-7g93 / PYSEC-2026-3493
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 OSV and the PyPI Advisory Database (CC-BY 4.0).
Pillow TGA RLE encoder can serialize up to ~57 KB of adjacent heap data into generated images
BIT-pillow-2026-59198 / CVE-2026-59198 / GHSA-fj7v-r99m-22gq / PYSEC-2026-3494
More information
Details
Summary
Pillow's TGA RLE encoder reads past its row buffer when saving a mode
"1"image. Adjacent process heap bytes can be copied into the generated TGA file.
The bug is reachable through the public save API:
Older affected Pillow versions use the equivalent public option
rle=True.For mode
"1", Pillow allocates a packed row buffer ofceil(width / 8)bytes, but
ImagingTgaRleEncode()treats the row as one full byte per pixel.The maximum valid TGA width is
65535. At that width:On non-ASAN Pillow
12.2.0, the public-only maximum-width PoC below serialized57297bytes from distinct out-of-bounds source offsets into one returned TGA,covering
99.92%of the maximum adjacent heap window. No heap grooming, ctypes,private API, or malformed input file was used. The disclosure is emitted across
many TGA packet payload copies of at most
128bytes each, not one largememcpy().Details
src/PIL/TgaImagePlugin.pyallows mode"1"TGA output and selects thetga_rleencoder when RLE compression is requested.src/encode.c:_setimage()allocates the row buffer using the packed-bitformula:
For mode
"1",state->bits == 1.src/libImaging/TgaRleEncode.cthen computes:This becomes
1, and the encoder uses pixel indexes as byte offsets:The packet payload
memcpy()later copies those out-of-bounds source bytes intothe output. Raw packets copy up to
128contiguous bytes, while RLE packets copyone representative byte:
A width-2 mode
"1"image allocates one row byte and already triggers an ASANheap-buffer-overflow read. Wider images increase the adjacent heap window and
the amount of heap data that can be serialized.
PoC
Minimal ASAN trigger
Observed on local Pillow
12.3.0.dev0ASAN target:Maximum-width heap disclosure
This PoC uses one maximum-width row. It parses the generated TGA packets and
extracts only payload bytes whose source offsets were outside the allocated
packed row. Rows are avoided because they mostly repeat the same adjacent heap window.
Run the following with a standard affected Pillow installation.
Observed on installed Pillow
12.2.0:Impact
This is a heap out-of-bounds read and potential information disclosure.
A maximum-width single-row image can cause nearly the full
57343-byte adjacent heap window to be incorporated into one output file.Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:LReferences
This data is provided by OSV and the PyPI Advisory Database (CC-BY 4.0).
Pillow: Decompression Bomb DoS via PdfParser.PdfStream.decode()
BIT-pillow-2026-59200 / CVE-2026-59200 / GHSA-jjj6-mw9f-p565 / PYSEC-2026-3495
More information
Details
Summary
PdfParser.PdfStream.decode()in Pillow'sPdfParser.pycallszlib.decompress()with thebufsizeparameter set to the value of the PDF stream'sLengthfield, without any upper bound on the actual decompressed output size. Python'szlib.decompress()bufsizeargument is an initial output buffer hint, not a maximum size limit — the function will expand memory until the full decompressed result is produced. A crafted PDF containing a FlateDecode-compressed stream decompresses to 1 GB of memory from a ~950 KB file, causing server OOM termination or severe degradation in any application that usesPdfParserto read untrusted PDF files.Details
PdfStream.decode()inpdfminer/PdfParser.pyreads the stream's declaredLength(orDL) field from the PDF dictionary and passes it asbufsizetozlib.decompress():From the Python documentation: "The
bufsizeparameter is used as the initial size of the output buffer." It does not cap decompression. An attacker who controls the PDF stream contents can provide a highly-compressed payload that expands to gigabytes, while settingLengthto any value (including the actual compressed size) to avoid triggering format validation.PdfParseris instantiated with a filename or file object and callsread_pdf_info()on open, which parses the xref table and makes stream objects accessible.PdfStream.decode()is reachable whenever calling code accesses a compressed stream object from the parsed PDF.Confirmed reachable path:
PoC
Actual output (Pillow 12.1.1, Python 3.12):
Measured expansion:
Impact
This is a denial-of-service vulnerability. Any application that uses
PIL.PdfParser.PdfParserto read untrusted PDF files is affected. An unauthenticated attacker who can submit a PDF for processing can exhaust all available server memory with a ~950 KB file, causing OOM termination or service degradation affecting all concurrent users. No authentication or user interaction beyond submitting the file is required.Note: This vulnerability is independent of CVE-2025-64512 / CVE-2025-70559 (pdfminer.six) and the companion
PIL/PdfImagePlugin.pydecompression issue. It exists specifically in Pillow's ownPdfParser.pymodule, which is distinct from pdfminer.six.Suggested fix:
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 OSV and the PyPI Advisory Database (CC-BY 4.0).
Pillow JPEG2000 tiled decode retains a growing scratch buffer and can be used for denial of service
BIT-pillow-2026-59204 / CVE-2026-59204 / GHSA-vjc4-5qp5-m44j / PYSEC-2026-3496
More information
Details
Summary
src/libImaging/Jpeg2KDecode.c:853accumulatestotal_component_widthacross every tile in a JPEG2000 image instead of recomputing it per tile. That accumulated value is then used in thetile_bytescalculation atsrc/libImaging/Jpeg2KDecode.c:868, which can make the decoder growstate->bufferviareallocatsrc/libImaging/Jpeg2KDecode.c:876up to roughly one full image's decompressed size even when each tile is small. A crafted tiled JPEG2000 file can therefore force substantially higher transient memory usage and trigger out-of-memory failures during decoding. Based on current evidence, the supported impact is denial of service, not memory corruption.Details
src/libImaging/Jpeg2KDecode.c:853total_component_widthis initialized only once before the tile loop and keeps growing across tiles. It is then used to derivetile_bytes, so later tiles are treated as if they had the combined component width of all earlier tiles.tile_bytesis promoted intotile_info.data_size, thenstate->bufferis grown withreallocatsrc/libImaging/Jpeg2KDecode.c:876.Image.open(...).load()decoding.PoC
The attached helper script and testcase were used:
exercise_j2k_tile_realloc.zip
Generate the testcase:
Expected geometry from the helper:
3664 x 3664RGBA1832 x 1832(2x2tiles)image_bytes=53699584maxrss_kb=180264maxrss_kb=138404Load it with the current vulnerable build:
Load it again under a 160 MB address-space cap:
Impact
Conservative impact: denial of service through memory exhaustion during JPEG2000 decoding.
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 OSV and the PyPI Advisory Database (CC-BY 4.0).
Release Notes
python-pillow/Pillow (pillow)
v12.3.0Compare Source
https://pillow.readthedocs.io/en/stable/releasenotes/12.3.0.html
Removals
Documentation
Dependencies
d872252#9614 [@renovate[bot]]Testing
tox -e lintinstead of pytest #9670 [@hugovk]Type hints
Other changes
Image.filter()#9736 [@akx]Image.getchannel(),Image.merge(),Image.putalpha()andImage.split()#9675 [@akx]Image.fill(),Image.linear_gradient()andImage.radial_gradient(). #9737 [@akx]Image.resample()#9739 [@akx]alpha_composite,matrix,negative,quantize#9740 [@akx]Image.blend()#9649 [@akx]IFDRational.__float__()return value #9676 [@nyxst4ck]RankFilter.ccheck #9662 [@radarhere]Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR has been generated by Mend Renovate.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.