{"id":"CVE-2026-96639","kind":"vulnerability","aliases":["CVE-2026-96639","GHSA-v9pv-5gc8-w2m6"],"related":[],"title":"Pillow multi-frame loaders can reuse stale native image storage after mode transitions","description":"### Summary\nPillow multi-frame image loaders can retain native `ImagingCore` storage allocated for one frame while changing the Python-side image `mode` and decoder tiles for a later frame.\n\nIf frame 0 allocates storage for a narrow mode such as `L`, and a later frame of the same dimensions switches to a wider mode such as `RGB` or `CMYK`, Pillow may decode wider pixels into the stale narrow native buffer. This causes native\nout-of-bounds writes and process crashes. In the MPO/JPEG path, I also demonstrated command execution in matching Python/Pillow runtime profiles through Pillow's documented `ImageSequence.Iterator` + `frame.load()` path.\n\n### Details\nThe root cause appears to be that `ImageFile.load_prepare()` only reallocates native storage when `_im is None`. It does not verify that the retained native `ImagingCore` storage still matches `self.mode` and `self.size`.\n\nSeveral multi-frame `seek()` / `_open()` paths can update `self._mode` and decoder tiles for a later frame without invalidating `_im`. The decoder then writes or reads according to the new frame mode while the native storage still belongs to the old frame mode.\n\nVerified affected surfaces:\n\n1. **PSD**\n\n   `PsdImageFile.seek()` can switch from grayscale/native `L` storage to RGB layer decoding while retaining `_im`. This produces attacker-controlled native out-of-bounds writes and reliable crashes.\n\n2. **MPO/JPEG**\n\n   A crafted two-frame MPO can make frame 0 allocate `L` storage, then make frame 1 decode `CMYK` / `RGB` data into the retained `L` storage. The documented `ImageSequence.Iterator` path reaches this with `frame.load()`.\n   Verified native memory corruption, crash, over-read behavior, and scoped harmless-marker command execution on Pillow 12.3.0.\n\n3. **DCX**\n\n   `DcxImageFile.seek()` calls `PcxImageFile._open()`, which changes mode/tile  state without a native-storage mode guard. A crafted DCX with `L` then `RGB` PCX frames produces retained-storage mismatch and crash.\n\n4. **MIC**\n\n   MIC can call `TiffImageFile._open()` directly and bypass TIFF's normal guarded `seek()` behavior. With a `NextIFD`-based reachable frame transition, this produces retained-storage mismatch and crash.\n\nTIFF appears to be a useful positive control because its `seek()` path checks\nboth size and mode before reusing storage.\n\n### Impact\n- process crash / denial of service,\n- native out-of-bounds write,\n- native over-read in the MPO path.\n\nStronger proven impact:\n\n- command execution in matching Python/Pillow runtime profiles through the MPO/JPEG path.\n\nServer-side deployment impact:\n\n- Create a local vulnerable web upload service that accepts an unauthenticated MPO upload and automatically processes it with Pillow.\n- The attack is one HTTP request and requires no user interaction after upload.\n- The fixed harmless command-output marker succeeded 10/10 times.\n- The `_im=None` control was clean 10/10 times.\n\n\n## Reproduction Evidence\n\nTested version:\n\n```text\nPillow 12.3.0\n```\n\nMPO terminal proof environments:\n\n```text\nPython 3.12.3 + Pillow 12.3.0, clean PyPI environment:\nmarker 10/10, control 0/10\n\nPython 3.13.14 + Pillow 12.3.0, prepared profile:\nmarker 10/10, control 0/10\n```\n\nServer-side web-upload proof:\n\n```text\ncontrol 1..10: MARKER_MISSING\ncontrol_summary ok 0 missing 10 /10\n\nvulnerable 1..10: MARKER_OK marker_output <server-user>\nvulnerable_summary ok 10 missing 0 /10\n```\n\nThe terminal proof uses a fixed harmless marker command only. It does not use a reverse shell, persistence, data exfiltration, or arbitrary attacker-selected commands.\n\nI can provide the following private artifacts to maintainers:\n\n- minimal crash fixtures for PSD, MPO, DCX, and MIC,\n- non-weaponized MPO marker PoC,\n- local vulnerable web upload demo,\n- gdb/GEF geometry receipts,\n- control-run logs,\n- proposed regression-test shape.\n\n## Minimal Reproduction Shape\n\nFor the MPO path, the vulnerable consumer shape is:\n\n```python\nfrom io import BytesIO\nfrom PIL import Image, ImageSequence\n\nsource = BytesIO(open(path, \"rb\").read())\nimage = Image.open(source)\niterator = ImageSequence.Iterator(image)\nnext(iterator).load()\nnext(iterator).load()\n```\n\nThe crafted image shape is:\n\n```text\nframe 0: mode L, dimensions W x H\nframe 1: same W x H, wider mode such as CMYK\n```\n\nFrame 0 allocates native `L` storage. Frame 1 changes the logical mode and decoder state but can reuse the old native storage. The frame 1 decoder writes using the wider mode into the narrow storage.\n\n## Suggested Fix Direction\n\nBefore reusing native `ImagingCore` storage, Pillow should verify that the retained storage is compatible with the current logical image mode and size.\n\nA central guard in `ImageFile.load_prepare()` could compare native storage mode/size against `self.mode` / `self.size` and allocate fresh storage when they differ. Alternatively, every mode-changing `seek()` / `_open()` path should explicitly invalidate `_im` before decoding.\n\nCaution: TIFF has specialized tile-size behavior and already has a mode/size guard, so a central fix should avoid regressing TIFF tile-buffer handling.\n\n\n## Credit\n\n```text\nRotem Kama\n```\n\n## Methodology Note\n\nAI-assisted analysis was used for discovery and verification support. Claims in this report are limited to the local artifacts, controls, and command outputs described above.","summary":"Pillow's multi-frame image loaders (PSD, MPO/JPEG, DCX, MIC) can reuse native image memory sized for an earlier frame's narrower mode when a later frame of the same size uses a wider mode. An attacker-supplied image file can trigger out-of-bounds memory writes that crash the process, and the reporter claims command execution through the MPO/JPEG path in a test environment.","severity":"critical","cvss":9.800000190734863,"epss":0.0056500001810491085,"exploited":false,"exploited_at":null,"products":[{"name":"Pillow","fixed":["13.0.0"],"affected":">= 11.0.0, < 13.0.0","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-125","cwe-787","image-processing","package","pillow","python","self-hosted","web-upload"],"not_affected_if":["no-untrusted-image-input","pillow-13.0.0-or-later","pillow-below-11.0.0"],"how_to_check":"Run python -c \"import PIL; print(PIL.__version__)\" in each service's environment and check whether it is >= 11.0.0 and < 13.0.0.\nRun pip show pillow or check the lockfile for the pinned Pillow version.\nIdentify services that open untrusted uploaded images with Pillow, especially multi-frame formats.","fix":"Upgrade Pillow to 13.0.0 or later.\nUntil then, avoid processing untrusted MPO, PSD, DCX or MIC files, or run that processing in an isolated sandbox.","status":"open","published_at":"2026-10-21T21:20:32+00:00","first_seen_at":"2026-10-23T19:11:31+00:00","triaged_at":"2026-10-23T19:50:19+00:00","updated_at":"2026-10-23T21:57:29+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-10-23T19:50:19Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade Pillow to 13.0.0 or later.\nUntil then, avoid processing untrusted MPO, PSD, DCX or MIC files, or run that processing in an isolated sandbox.","summary":"Pillow's multi-frame image loaders (PSD, MPO/JPEG, DCX, MIC) can reuse native image memory sized for an earlier frame's narrower mode when a later frame of the same size uses a wider mode. An attacker-supplied image file can trigger out-of-bounds memory writes that crash the process, and the reporter claims command execution through the MPO/JPEG path in a test environment.","category":"package","who_cares":["pillow","python","self-hosted","web-upload","image-processing"],"confidence":0.85,"analyst_note":"","how_to_check":"Run python -c \"import PIL; print(PIL.__version__)\" in each service's environment and check whether it is >= 11.0.0 and < 13.0.0.\nRun pip show pillow or check the lockfile for the pinned Pillow version.\nIdentify services that open untrusted uploaded images with Pillow, especially multi-frame formats.","needs_analyst":false,"not_affected_if":["pillow-below-11.0.0","pillow-13.0.0-or-later","no-untrusted-image-input"],"severity_if_unknown":"critical"},"cost_usd":0.000883,"input_tokens":4014,"output_tokens":964,"severity_before":"critical"},"analyst_status":null,"analyst_note":null,"search":"'-125':28B '-2026':16A,24A '-787':30B '-96639':17A,25A '/10':602C,616C '0':144C,335C,599C,615C '0/10':572C,582C '1':294C,342C,592C,604C '10':593C,601C,605C,613C '10/10':540C,548C,570C,580C '12.3.0':378C,556C,565C,576C '2':324C '3':379C '3.12.3':563C '3.13.14':574C '4':411C '5gc8':21A 'accept':508C 'accord':274C 'affect':292C 'alloc':123C,145C,336C 'also':199C 'appear':219C,442C 'arbitrari':638C 'artifact':649C 'attack':81B,313C,520C,640C 'attacker-control':312C 'attacker-select':639C 'attacker-suppli':80B 'automat':514C 'base':429C 'behavior':368C,425C 'belong':285C 'bound':90B,189C,319C,471C 'buffer':182C 'bypass':419C 'bytesio':700C 'call':382C,415C 'caus':184C,218C 'chang':128C,385C 'check':453C 'claim':100B 'clean':547C,566C 'cmyk':171C,344C 'command':101B,201C,374C,484C,536C,625C,642C 'command-output':535C 'consum':693C 'control':314C,448C,545C,571C,581C,591C,596C,676C 'control-run':675C 'corrupt':363C 'craft':327C,396C 'crash':94B,193C,323C,364C,410C,440C,463C,653C 'creat':500C 'cve':15A,23A 'cwe':27B,29B 'data':346C,635C 'dcx':52B,380C,397C,658C 'dcximagefile.seek':381C 'decod':136C,174C,259C,269C,306C,343C 'demo':671C 'demonstr':200C 'denial':464C 'deploy':498C 'detail':215C 'dimens':161C 'direct':417C 'document':211C,353C 'earlier':62B 'environ':110B,560C,568C 'evid':551C 'execut':102B,202C,375C,485C 'exfiltr':636C 'file':84B 'fix':533C,622C 'fixtur':654C 'follow':647C 'frame':4A,47B,63B,70B,115C,126C,141C,143C,157C,251C,264C,278C,289C,330C,334C,341C,403C,431C 'frame.load':213C,359C 'gdb/gef':672C 'geometri':673C 'ghsa':19A 'ghsa-v9pv-5gc8-w2m6':18A 'grayscale/native':300C 'guard':394C,423C 'harmless':372C,534C,623C 'harmless-mark':371C 'host':39B 'http':523C 'im':230C,267C,309C,543C 'imag':10A,32B,48B,57B,83B,116C,133C,704C 'image-process':31B 'imagefile.load':223C 'imagesequ':705C 'imagesequence.iterator':212C,354C 'imagingcor':121C,241C 'impact':461C,483C,499C 'import':699C,703C 'interact':529C 'invalid':266C 'io':698C 'l':153C,301C,337C,350C,399C 'later':69B,140C,156C,263C 'layer':305C 'loader':5A,49B,117C 'local':502C,667C 'log':678C 'maintain':651C 'make':333C,340C 'marker':373C,538C,569C,579C,594C,606C,608C,624C,665C 'match':204C,244C,487C 'may':173C 'memori':58B,91B,362C 'mic':53B,412C,413C,660C 'minim':652C,684C 'mismatch':408C,438C 'miss':595C,600C,614C 'mode':13A,66B,78B,134C,150C,166C,279C,290C,393C,457C 'mode/tile':386C 'mpo':331C,479C,511C,557C,657C,664C,689C 'mpo/jpeg':51B,105B,196C,325C,493C 'multi':3A,46B,114C,250C 'multi-fram':2A,45B,113C,249C 'narrow':65B,149C,180C 'nativ':9A,56B,120C,181C,185C,227C,240C,282C,315C,361C,391C,467C,473C 'native-storag':390C 'new':277C 'nextifd':428C 'non':662C 'non-weapon':661C 'none':232C,544C 'normal':422C 'ok':598C,607C,612C 'old':288C 'one':125C,522C 'open':253C 'out-of-bound':87B,186C,316C,468C 'output':537C,609C 'over-read':365C,474C 'packag':34B 'path':106B,197C,214C,254C,355C,452C,480C,494C,690C 'pcx':402C 'pcximagefile._open':383C 'persist':634C 'pil':702C 'pillow':1A,26A,35B,43B,112C,172C,209C,377C,518C,555C,564C,575C 'pixel':176C 'poc':666C 'posit':447C 'prepar':224C,577C 'privat':648C 'process':33B,96B,192C,462C,515C 'produc':311C,404C,434C 'profil':207C,490C,578C 'proof':559C,589C,619C 'propos':679C 'proven':482C 'provid':645C 'psd':50B,295C,656C 'psdimagefile.seek':296C 'pypi':567C 'python':36B,131C,562C,573C,696C 'python-sid':130C 'python/pillow':205C,488C 'reach':356C 'reachabl':430C 'read':273C,367C,476C 'realloc':226C 'receipt':674C 'regress':681C 'regression-test':680C 'reliabl':322C 'report':99B 'reproduct':550C,685C 'request':524C 'requir':526C 'retain':119C,239C,308C,349C,406C,436C 'retained-storag':405C,435C 'reus':7A,55B,459C 'revers':632C 'rgb':169C,304C,345C,401C 'root':217C 'run':677C 'runtim':206C,489C 'scope':370C 'seek':252C,424C,451C 'select':641C 'self':38B 'self-host':37B 'self._mode':257C 'self.mode':245C 'self.size':247C 'server':496C,584C 'server-sid':495C,583C 'servic':466C,506C 'sever':248C 'shape':683C,686C,694C 'shell':633C 'side':132C,497C,585C 'size':59B,74B,455C 'sourc':706C 'stale':8A,179C 'state':387C 'still':243C,284C 'storag':11A,122C,146C,228C,242C,283C,302C,338C,351C,392C,407C,437C,460C 'stronger':481C 'succeed':539C 'summari':111C,597C,611C 'suppli':82B 'surfac':293C 'switch':162C,298C 'termin':558C,618C 'test':109B,552C,682C 'text':554C,561C,590C 'tiff':420C,441C 'tiffimagefile._open':416C 'tile':137C,260C 'time':541C,549C 'transit':14A,432C 'trigger':86B 'two':329C 'two-fram':328C 'unauthent':510C 'updat':256C 'upload':42B,505C,512C,531C,588C,670C 'use':75B,446C,620C,630C 'user':528C 'v9pv':20A 'verifi':236C,291C,360C 'version':553C 'vulner':503C,603C,610C,668C,692C 'w2m6':22A 'weapon':663C 'web':41B,504C,587C,669C 'web-upload':40B,586C 'wider':77B,165C,175C 'without':265C,388C 'write':92B,190C,271C,320C,472C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-21T21:20:32+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-v9pv-5gc8-w2m6","url":"https://github.com/advisories/GHSA-v9pv-5gc8-w2m6","severity":"critical","published_at":"2026-10-22T16:00:15+00:00","modified_at":"2026-10-22T16:00:15+00:00","first_seen_at":"2026-10-23T19:11:31+00:00","fetched_at":"2026-10-23T19:11:31+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-96639","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-96639","severity":null,"published_at":"2026-10-21T21:20:32+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"}