Patch Forecast
Yellow warning
Plan into this sprint.

CVE-2026-112899 · GHSA-vx6w-vv9f-6gvg

Pillow: EPS load(scale) lets Ghostscript render past the decompression bomb limit

Risk score
46/100
CVSS
6.9 v4.0
Published
20 Oct 2026

Open in the live forecast Today's forecast

What to know

What it is
EPS load(scale) lets Ghostscript render past the decompression bomb limit Location: src/PIL/EpsImagePlugin.py:92 in Ghostscript() Commit: a707c6b21c313880d0771e54965d142dd42358b6 When an application opens an untrusted Encapsulated PostScript file and asks Pillow to render it at a larger scale, Pillow has Ghostscript p…
Affected
Pillow (pip) >= 2.3.0, <= 12.3.0
Fix
Upgrade Pillow to 13.0.0 or later.
Exploitation
No known exploitation

Story map

Story map: PillowTHE FLAWSTHE FIXCVE-2026-112899EPS load(scale) letsGhostscript render past thedecompression bomb limitCVSS 6.9CVE-2026-112393Heap buffer overflow in_tiffWriteProc when savingTIFF to BytesIOCVSS 9.2 · EPSS 1%CVE-2026-96639Pillow multi-frame loaders canreuse stale native imagestorage after mode transitionsCVSS 9.8 · EPSS 1%CVE-2026-112900Argument Injection inGhostscript call, potentiallyleading into RCECVSS 9.2 · EPSS 1%Upgrade Pillow13.0.0or later on your release lineWho is exposed{'products': ['Pillow'],'packages': [{'ecosystem': 'pip','name': 'Pillow', 'vulnerable': '>=2.0.0, < 13.0.0'}], 'versions':['2.0.0', '13.0.0']}RED · ACT TODAYPillowPillow has a heap bufferoverflow and stale-heapdisclosure in _tiffWriteProcwhen saving compressed TIFFdata. This can crash the processand may allow code execution.Where it standsPublic proof of concept1 outlet covering itHOW IT UNFOLDEDOfficial record on the left, the press on the right, oldest first.23 Oct · The Hacker NewsCritical Flaw in Python’s Popular Pillow ImageLibrary Allows Code Execution via a Crafted ImageCONNECTS TOSame weaknessDenial of service (CWE-770)206 other warnings in 30 days →
Story map: the flaws, the fix, how it unfolded and what it connects to. Boxes link to the record or article.

At a glance

Pillow: Red, act today. Pillow has a heap buffer overflow and stale-heap disclosure in _tiffWriteProc when saving compressed TIFF data. This can crash the process and may allow code execution.

The flaws

  • CVE-2026-112899: EPS load(scale) lets Ghostscript render past the decompression bomb limit (CVSS 6.9)
  • CVE-2026-112393: Heap buffer overflow in _tiffWriteProc when saving TIFF to BytesIO (CVSS 9.2 · EPSS 1%)
  • CVE-2026-96639: Pillow multi-frame loaders can reuse stale native image storage after mode transitions (CVSS 9.8 · EPSS 1%)
  • CVE-2026-112900: Argument Injection in Ghostscript call, potentially leading into RCE (CVSS 9.2 · EPSS 1%)

The fix

Upgrade Pillow: 13.0.0 or later on your release line

Who is exposed: {'products': ['Pillow'], 'packages': [{'ecosystem': 'pip', 'name': 'Pillow', 'vulnerable': '>= 2.0.0, < 13.0.0'}], 'versions': ['2.0.0', '13.0.0']}

Where it stands

  • Public proof of concept
  • 1 outlet covering it

How it unfolded

  1. 23 Oct, The Hacker News (press): Critical Flaw in Python’s Popular Pillow Image Library Allows Code Execution via a Crafted Image

Connects to

Why it is Yellow

  • CVSS 6.9 medium
  • On your watchlist: PyPI

Affected packages

PackageEcosystemVulnerableFixed in
Pillowpip>= 2.3.0, <= 12.3.013.0.0

References

Original description

``` ============================================================================ EPS load(scale) lets Ghostscript render past the decompression bomb limit Location: src/PIL/EpsImagePlugin.py:92 in Ghostscript() Commit: a707c6b21c313880d0771e54965d142dd42358b6 ============================================================================ ``` When an application opens an untrusted Encapsulated PostScript file and asks Pillow to render it at a larger scale, Pillow has Ghostscript produce an image several times bigger than Pillow's configured size limit allows. The size check runs only after Ghostscript has finished rendering and written the full raw image to a temporary file. An attacker who can upload a file of about 100 bytes makes each such request spend scale² times the Ghostscript rendering work and temporary disk space that the limit is meant to permit. For example, at scale 2 that is a 972 MB temporary file. The cause is in `Ghostscript()` in `src/PIL/EpsImagePlugin.py`. It multiplies the file-declared size by the caller's `scale` (lines 93-94) and passes the product to `gs` as the `-g` output geometry. It never calls `Image._decompression_bomb_check()` on the scaled size. The only check is the one `Image.open()` ran on the unscaled size, so a file declaring 9000×9000 passes, and `load(scale=2)` then asks Ghostscript for 18000×18000 (3.6× `MAX_IMAGE_PIXELS`). A file declaring 18000×18000 directly would be refused before Ghostscript ever ran. `DecompressionBombError` fires only when Pillow reopens the output file, so Pillow's own in-process allocation stays bounded and there is no memory corruption. The effect is a constant-factor overshoot rather than an unbounded one, for three reasons: * The multiplier is an integer chosen by the application, not by the file. * With the default warning filter, `Image.open()` already lets files up to 2× the limit reach Ghostscript at `scale=1`. * Any application feeding untrusted files to Ghostscript is already exposed to a tiny file that keeps Ghostscript busy indefinitely, since `gs` runs with no timeout. This is the EPS counterpart of the WMF `load(dpi)` gap closed in af70b075a. It has been present since the decompression-bomb limit was introduced in ce2955ec7 (2.5.0). The attached reproducer stubs out Ghostscript and shows a 76-byte file passing `Image.open()` and reaching `gs` with `-g18000x18000`. A one-line patch mirroring the WMF check is included. ## Details `EpsImageFile.load(scale=N)` (the documented way to rasterise an EPS at higher resolution) renders `width*N × height*N` pixels through Ghostscript, but the only decompression-bomb check on the way is the one `Image.open()` ran on the *unscaled*, file-declared `width × height`. The scaled product is never passed to `Image._decompression_bomb_check()` before `gs` is started. This 111-byte EPS declares 9000×9000 = 81,000,000 pixels, below the default `MAX_IMAGE_PIXELS` (89,478,485), so `Image.open()` accepts it without even a warning: ``` %!PS-Adobe-3.0 EPSF-3.0 %%BoundingBox: 0 0 9000 9000 %%EndComments 0 0 moveto 9000 9000 lineto stroke showpage ``` `im.load(scale=2)` on it then executes ``` gs -q -g18000x18000 -r144.000000x144.000000 -dBATCH -dNOPAUSE -dSAFER -sDEVICE=pnmraw -sOutputFile=/tmp/tmpXXXX -c 0 0 translate -f attack.eps -c showpage ``` i.e. asks Ghostscript for an 18000×18000 = 324,000,000-pixel raster (3.6× `MAX_IMAGE_PIXELS`), whose `pnmraw` output is a 972,000,019-byte file in the temp directory. An EPS that declares 18000×18000 directly is rejected by `Image.open()` with `DecompressionBombError` before Ghostscript is ever run; via `scale=2` the same raster is rendered first and the `DecompressionBombError` only appears afterwards, when Pillow opens the file Ghostscript produced. (Between 1× and 2× `MAX_IMAGE_PIXELS`, `Image.open()` only emits `DecompressionBombWarning`, so with the default warning filter an EPS declaring e.g. 13377×13377 is already rendered at `scale=1` — a 537 MB PNM. Against that baseline, `scale=2` is 4× an already-permitted render.) The arithmetic is in `Ghostscript()`: ```python # src/PIL/EpsImagePlugin.py:91-97 # Hack to support hi-res rendering scale = int(scale) or 1 width = size[0] * scale height = size[1] * scale # resolution is dependent on bbox and size res_x = 72.0 * width / (bbox[2] - bbox[0]) … # src/PIL/EpsImagePlugin.py:139-142 command = [ gs_binary, "-q", # quiet mode f"-g{width:d}x{height:d}", # set output geometry (pixels) … # src/PIL/EpsImagePlugin.py:165-168 subprocess.check_call(command, startupinfo=startupinfo) with Image.open(outfile) as out_im: out_im.load() return out_im.im.copy() ``` and its caller does nothing between taking `scale` from the application and invoking it: ```python # src/PIL/EpsImagePlugin.py:411-417 def load(self, scale: int = 1, transparency: bool = False) -> …: # Load EPS via Ghostscript if self.tile: assert self.fp is not None self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) ``` `size` here is `self._size`, computed in `_open()` from the file's `%%BoundingBox` (or `%ImageData`) and checked once by `Image.open()` at `src/PIL/Image.py:3700`. For the benign path (`scale=1`) that check covers exactly what Ghostscript is asked to render. For `scale=N` the rendered pixel count is N² times what was checked, so a file can sit just under the limit at open time and have Ghostscript render 4× (scale 2), 9× (scale 3), 16× (scale 4) the configured budget. The first point at which Pillow looks at the real size again is `Image.open(outfile)` at line 166 — after `subprocess.check_call` has returned, i.e. after Ghostscript has spent the CPU and written `width*height*3` bytes to `tempfile.mkstemp()`'s directory. `WmfStubImageFile.load(dpi)` has the same shape (file-derived extent × caller-supplied factor → new raster) and, since af70b075a,

Last activity 20 Oct 2026. Patch Forecast updates every hour.