{"id":"CVE-2026-96636","kind":"vulnerability","aliases":["CVE-2026-96636","GHSA-p8hx-v2p6-rp84"],"related":[],"title":"Pillow: Decompression-bomb protection bypass via multi-frame seek() in MPO and DCX (MpoImageFile.seek / DcxImageFile.seek)","description":"### Summary\nPillow's decompression-bomb protection (`Image._decompression_bomb_check`, gated by `Image.MAX_IMAGE_PIXELS`) is enforced only on the **first frame**, inside `Image.open()`. For multi-frame formats, `seek()` re-parses the target frame's header and resets `self._size`. `GifImagePlugin` and `TiffImagePlugin` re-run the bomb check on `seek()`, but **`MpoImageFile.seek()` and `DcxImageFile.seek()` do not.** A crafted MPO or DCX whose first frame is tiny (passing the check) but whose later frame declares enormous dimensions therefore bypasses the protection: loading that frame decodes an over-limit image and commits large amounts of memory — a denial of service (memory exhaustion / OOM).\n\nSame class as the recently assigned CVE-2026-55379 (BdfFontFile), CVE-2026-55380 (GdImageFile) and CVE-2026-40192 (FITS GZIP).\n\n### Details\n`Image.open()` → `_open_core()` runs `_decompression_bomb_check(im.size)` once, on the frame present at open time. Both vulnerable `seek()` methods re-parse a new frame and set a new `self._size` without re-checking:\n\n- **`PIL/MpoImagePlugin.py`, `MpoImageFile.seek()`**: calls `JpegImagePlugin.JpegImageFile._open(self)` (which sets `self._size` from the frame's JPEG SOF) and rebuilds `self.tile`, with no `Image._decompression_bomb_check(self.size)`.\n- **`PIL/DcxImagePlugin.py`, `DcxImageFile.seek()`**: calls `PcxImageFile._open(self)` (which sets `self._size` from the page's PCX header), with no check.\n\nConfirmed present in **12.3.0** (latest release) and on **`main`** (13.0.0.dev0). Compare `GifImagePlugin._seek` and `TiffImagePlugin`, which call `_decompression_bomb_check` correctly on seek.\n\n### PoC\nSelf-contained; the victim uses **default** `MAX_IMAGE_PIXELS`:\n\n```python\nimport io\nfrom PIL import Image\n\n# attacker builds the file (guard disabled only to CREATE the oversized frame)\nImage.MAX_IMAGE_PIXELS = None\nsmall = Image.new(\"RGB\", (8, 8))\nbig   = Image.new(\"RGB\", (14000, 14000), (120, 60, 30))   # 196,000,000 px > error threshold\nbuf = io.BytesIO()\nsmall.save(buf, format=\"MPO\", save_all=True, append_images=[big])\ndata = buf.getvalue()                                      # ~3 MB\nImage.MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3) # restore default\n\n# victim (default limits)\nim = Image.open(io.BytesIO(data))   # frame 0 (8x8) passes the bomb check\nim.seek(1)                          # frame 1 (14000x14000) -> NOT re-checked  <-- bug\nim.load()                           # decodes the over-limit image, commits memory\nprint(\"loaded over-limit frame:\", im.size)\n```\n\nMeasured on Pillow 12.3.0 (default limit 89,478,485 px; error threshold 178,956,970 px):\n\n| Vector | File size | Frame 1 | Peak RSS |\n|---|---|---|---|\n| MPO (JPEG) | ~3.0 MB | 14000×14000 (196 Mpx) | ~763 MB |\n| DCX (PCX) | ~6.2 MB | 14000×14000 (196 Mpx) | ~200 MB |\n| Control: same image standalone | — | 196 Mpx | `DecompressionBombError` (correctly blocked) |\n\nThe DCX vector (`0x3ADE68B1` magic + offset directory + two PCX pages) behaves identically via `DcxImageFile.seek()`. Scaling frame 1 toward JPEG's ~65500×65500 maximum drives multi-GB commitments from a small file, reliably OOM-killing the process.\n\n### Impact\nDenial of service (memory exhaustion / OOM) — CWE-400 / CWE-1284, a bypass of Pillow's documented decompression-bomb mitigation. `seek()` + `load()` is the normal way to iterate frames (e.g. `PIL.ImageSequence.Iterator`, thumbnailers, converters, 3D-photo handlers). Because **MPO is auto-detected from a JPEG carrying MPF metadata**, a file served with a `.jpg` extension can be opened as MPO, so any application that iterates frames of untrusted images is exposed.\n\n### Suggested fix\nRe-run the check after `seek()` re-parses a frame, mirroring GIF/TIFF:\n\n```python\n# PIL/MpoImagePlugin.py — end of MpoImageFile.seek()\nself.tile = [ImageFile._Tile(\"jpeg\", (0, 0) + self.size, self.offset, self.tile[0][-1])]\nImage._decompression_bomb_check(self.size)   # add\nself.__frame = frame\n\n# PIL/DcxImagePlugin.py — end of DcxImageFile.seek()\nPcxImageFile._open(self)\nImage._decompression_bomb_check(self.size)   # add\n```","summary":"Pillow's decompression-bomb check only runs on the first frame of MPO and DCX images, so seek() to a later frame skips it. A crafted file with a tiny first frame and a huge later frame can make the process allocate very large amounts of memory, causing denial of service (out-of-memory).","severity":"medium","cvss":6.5,"epss":0.00279999990016222,"exploited":false,"exploited_at":null,"products":[{"name":"Pillow","fixed":["13.0.0"],"affected":"< 13.0.0","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-400","cwe-409","image-processing","package","pillow","python","self-hosted"],"not_affected_if":["no-multi-frame-iteration","no-untrusted-image-input","pillow-13.0.0-or-later"],"how_to_check":"Run pip show pillow and check whether the version is below 13.0.0.\nSearch the code for Image.open calls followed by seek() on untrusted MPO or DCX files.","fix":"Upgrade Pillow to 13.0.0 or later (pip install -U pillow).\nUntil then, do not iterate frames of untrusted MPO or DCX files, or reject them before opening.","status":"open","published_at":"2026-10-21T21:20:31+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T19:57:42+00:00","updated_at":"2026-10-23T21:57:29+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T19:57:42Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade Pillow to 13.0.0 or later (pip install -U pillow).\nUntil then, do not iterate frames of untrusted MPO or DCX files, or reject them before opening.","summary":"Pillow's decompression-bomb check only runs on the first frame of MPO and DCX images, so seek() to a later frame skips it. A crafted file with a tiny first frame and a huge later frame can make the process allocate very large amounts of memory, causing denial of service (out-of-memory).","category":"package","who_cares":["python","pillow","self-hosted","image-processing"],"confidence":0.8,"analyst_note":"","how_to_check":"Run pip show pillow and check whether the version is below 13.0.0.\nSearch the code for Image.open calls followed by seek() on untrusted MPO or DCX files.\nunknown","needs_analyst":false,"not_affected_if":["pillow-13.0.0-or-later","no-untrusted-image-input","no-multi-frame-iteration"],"severity_if_unknown":"unknown"},"cost_usd":0.000565,"input_tokens":3846,"output_tokens":361,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-1':632C '-1284':539C '-2026':19A,27A,208C,212C,217C '-400':31B,537C '-40192':218C '-409':33B '-55379':209C '-55380':213C '-96636':20A,28A '0':407C,626C,627C,631C '000':367C,368C '0x3ade68b1':494C '1':414C,416C,459C,507C '1024':392C,393C,394C '12.3.0':299C,442C '120':363C '13.0.0':305C '14000':361C,362C,466C,467C,476C,477C '14000x14000':417C '178':451C '196':366C,468C,478C,486C '200':480C '3':386C,396C '3.0':464C '30':365C '3d':564C '3d-photo':563C '4':395C '478':446C '485':447C '6.2':474C '60':364C '65500':511C,512C '763':470C '8':356C,357C '89':445C '8x8':408C '956':452C '970':453C 'add':635C 'alloc':85B 'amount':88B,191C 'append':381C 'applic':593C 'assign':206C 'attack':337C 'auto':571C 'auto-detect':570C 'bdffontfil':210C 'behav':501C 'big':358C,383C 'block':490C 'bomb':4A,47B,104C,145C,227C,314C,411C,548C 'buf':372C,375C 'buf.getvalue':385C 'bug':422C 'build':338C 'bypass':6A,176C,541C 'call':259C,281C,312C 'carri':576C 'caus':91B 'check':48B,146C,167C,228C,256C,295C,315C,412C,421C,608C 'class':202C 'commit':189C,430C,518C 'compar':307C 'confirm':296C 'contain':322C 'control':482C 'convert':562C 'core':224C 'correct':316C,489C 'craft':69B,156C 'creat':345C 'cve':18A,26A,207C,211C,216C 'cwe':30B,32B,536C,538C 'data':384C,405C 'dcx':15A,58B,159C,472C,492C 'dcxima':641C 'dcximagefile.seek':17A,152C,280C,504C 'declar':172C 'decod':182C,424C 'decompress':3A,46B,103C,226C,313C,547C 'decompression-bomb':2A,45B,102C,546C 'decompressionbomberror':488C 'default':326C,398C,400C,443C 'denial':92B,195C,530C 'detail':221C 'detect':572C 'dev0':306C 'dimens':174C 'directori':497C 'disabl':342C 'document':545C 'drive':514C 'e.g':559C 'end':620C,639C 'enforc':113C 'enorm':173C 'error':370C,449C 'exhaust':199C,534C 'expos':601C 'extens':585C 'file':70B,340C,456C,522C,580C 'first':53B,74B,117C,161C 'fit':219C 'fix':603C 'format':125C,376C 'frame':10A,54B,65B,75B,80B,118C,124C,132C,162C,171C,181C,233C,247C,268C,348C,406C,415C,437C,458C,506C,558C,596C,615C,637C 'gate':107C 'gb':517C 'gdimagefil':214C 'ghsa':22A 'ghsa-p8hx-v2p6-rp84':21A 'gif/tiff':617C 'gifimageplugin':138C 'gifimageplugin._seek':308C 'guard':341C 'gzip':220C 'handler':566C 'header':134C,292C 'host':42B 'huge':78B 'ident':502C 'im':402C 'im.load':423C 'im.seek':413C 'im.size':229C,438C 'imag':35B,59B,110C,187C,328C,336C,350C,382C,389C,429C,484C,599C 'image-process':34B 'image._decompression_bomb_check':106C,277C,633C 'image.max':109C,349C,388C 'image.new':354C,359C 'image.open':120C,222C,403C 'imagefile._tile':624C 'impact':529C 'import':331C,335C 'insid':119C 'int':391C 'io':332C 'io.bytesio':373C,404C 'iter':557C,595C 'jpeg':270C,463C,509C,575C,625C 'jpegimageplugin.jpegimagefile':260C 'jpg':584C 'kill':526C 'larg':87B,190C 'later':64B,79B,170C 'latest':300C 'limit':186C,401C,428C,436C,444C 'load':179C,433C,551C 'magic':495C 'main':304C 'make':82B 'max':327C 'maximum':513C 'mb':387C,465C,471C,475C,481C 'measur':439C 'memori':90B,98B,193C,198C,431C,533C 'metadata':578C 'method':241C 'mirror':616C 'mitig':549C 'mpf':577C 'mpo':13A,56B,157C,377C,462C,568C,590C 'mpoimagefile.seek':16A,150C,258C,622C 'mpx':469C,479C,487C 'multi':9A,123C,516C 'multi-fram':8A,122C 'multi-gb':515C 'new':246C,251C 'none':352C 'normal':554C 'offset':496C 'oom':200C,525C,535C 'oom-kil':524C 'open':223C,236C,261C,588C 'out-of-memori':95B 'over-limit':184C,426C,434C 'overs':347C 'p8hx':23A 'packag':37B 'page':289C,500C 'pars':129C,244C,613C 'pass':165C,409C 'pcx':291C,473C,499C 'pcximagefile._open':282C 'peak':460C 'photo':565C 'pil':334C 'pil.imagesequence.iterator':560C 'pil/dcximageplugin.py':279C,638C 'pil/mpoimageplugin.py':257C,619C 'pillow':1A,29A,38B,43B,100C,441C,543C 'pixel':111C,329C,351C,390C 'poc':319C 'present':234C,297C 'print':432C 'process':36B,84B,528C 'protect':5A,105C,178C 'px':369C,448C,454C 'python':39B,330C,618C 're':128C,142C,243C,255C,420C,605C,612C 're-check':254C,419C 're-pars':127C,242C,611C 're-run':141C,604C 'rebuild':273C 'recent':205C 'releas':301C 'reliabl':523C 'reset':136C 'restor':397C 'rgb':355C,360C 'rp84':25A 'rss':461C 'run':50B,143C,225C,606C 'save':378C 'scale':505C 'seek':11A,61B,126C,148C,240C,318C,550C,610C 'self':41B,262C,283C,321C 'self-contain':320C 'self-host':40B 'self.__frame':636C 'self._size':137C,252C,265C,286C 'self.offset':629C 'self.size':278C,628C,634C 'self.tile':274C,623C,630C 'serv':581C 'servic':94B,197C,532C 'set':249C,264C,285C 'size':457C 'skip':66B 'small':353C,521C 'small.save':374C 'sof':271C 'standalon':485C 'suggest':602C 'summari':99C 'target':131C 'therefor':175C 'threshold':371C,450C 'thumbnail':561C 'tiffimageplugin':140C,310C 'time':237C 'tini':73B,164C 'toward':508C 'true':380C 'two':498C 'untrust':598C 'use':325C 'v2p6':24A 'vector':455C,493C 'via':7A,503C 'victim':324C,399C 'vulner':239C 'way':555C 'whose':160C,169C 'without':253C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-21T21:20:31+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-p8hx-v2p6-rp84","url":"https://github.com/advisories/GHSA-p8hx-v2p6-rp84","severity":"medium","published_at":"2026-10-22T15:56:37+00:00","modified_at":"2026-10-22T15:56:39+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-96636","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-96636","severity":null,"published_at":"2026-10-21T21:20:31+00:00","modified_at":"2026-10-22T16:46:48+00:00","first_seen_at":"2026-10-23T21:57:29+00:00","fetched_at":"2026-10-23T22:29:02+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"}