CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-128067

CVE-2026-128067: Out-of-Bounds Heap Read and Write in Pillow libtiff Encoder via Geometry Tag Overrides

Amit Schendel
Amit Schendel
Senior Security Researcher

Nov 13, 2026·10 min read·0 visits

Executive Summary (TL;DR)

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.

Vulnerability Overview

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.

Root Cause Analysis

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.

Code Analysis

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

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.

Primitive 1: Heap Memory Disclosure (Out-of-Bounds Read)

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.

Primitive 2: Denial of Service (Process Crash)

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")

Primitive 3: Heap Corruption (Out-of-Bounds Write)

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.

Impact Assessment

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.

Remediation

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.

Official Patches

python-pillowGitHub Security Advisory GHSA-xcvg-w56q-r89m
python-pillowPillow 13.0.0 Release Notes
python-pillowUpstream Fix Pull Request #10226

Fix Analysis (2)

Technical Appendix

CVSS Score
8.4/ 10
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 Systems

Pillow (Python Imaging Library fork) >= 2.3.0, <= 12.3.0

Affected Versions Detail

Product
Affected Versions
Fixed Version
Pillow
python-pillow
>= 2.3.0, <= 12.3.013.0.0
AttributeDetail
Vulnerability IDCVE-2026-128067 / GHSA-xcvg-w56q-r89m
Weakness ClassCWE-787 (Out-of-bounds Write) / CWE-125 (Out-of-bounds Read)
CVSS v4.0 Score8.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 ComponentPillow TiffImagePlugin._save & ImagingLibTiffEncode (TiffDecode.c)
Affected VersionsPillow >= 2.3.0, <= 12.3.0
Patched VersionPillow 13.0.0
ImpactHeap memory disclosure into output TIFF, process crash (DoS), and heap corruption
Exploit StatusProof-of-Concept Verified (Not in CISA KEV)

MITRE ATT&CK Mapping

T1203Exploitation for Client Execution
Execution
T1068Exploitation for Privilege Escalation
Privilege Escalation
CWE-787
Out-of-bounds Write

The software writes data past the end, or before the beginning, of the intended buffer.

Vulnerability Timeline

Vulnerability reported by carlini with reproduction cases.
2026-10-12
Initial fix commit ec5f1cbcf67275641abaad5c065ffb64a0a804a9 authored.
2026-10-13
Pull Request #10226 merged into main branch via commit 53318b50e8e9e85a57d033d4d879e1398d97aeb1.
2026-10-16
Pillow 13.0.0 released and GitHub Security Advisory GHSA-xcvg-w56q-r89m published.
2026-11-12
CVE-2026-128067 officially published by GitHub_M CNA.
2026-11-12
NVD record populated for CVE-2026-128067.
2026-11-13

References & Sources

  • [1]GitHub Security Advisory GHSA-xcvg-w56q-r89m
  • [2]Pillow Merge Commit 53318b50e8e9e85a57d033d4d879e1398d97aeb1
  • [3]Pillow Fix Commit ec5f1cbcf67275641abaad5c065ffb64a0a804a9
  • [4]Pillow 13.0.0 Release
  • [5]Pillow Pull Request #10226
  • [6]CVE.org Record for CVE-2026-128067
  • [7]NVD Entry for CVE-2026-128067
  • [8]CVE List V5 JSON Record for CVE-2026-128067

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•about 2 hours ago•CVE-2026-128066
8.7

CVE-2026-128066: Quadratic Memory Amplification Denial of Service in Pillow TIFF and EXIF IFD Parser

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).

Amit Schendel
Amit Schendel
2 views•13 min read
•about 3 hours ago•CVE-2026-126081
7.5

CVE-2026-126081: Strong Parameters Bypass via Parameters#each_key in Ruby on Rails Action Pack

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.

Amit Schendel
Amit Schendel
3 views•9 min read
•about 4 hours ago•CVE-2026-126084
4.7

CVE-2026-126084: Unsigned Content-Type in Rails Active Storage GCS Direct Uploads

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.

Amit Schendel
Amit Schendel
6 views•9 min read
•about 5 hours ago•CVE-2026-126079
6.1

CVE-2026-126079: Cross-Site Scripting in Ruby on Rails Action View Text Helpers via HTML5 Serialization Desynchronization

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.

Amit Schendel
Amit Schendel
3 views•10 min read
•about 6 hours ago•CVE-2026-126070
6.1

CVE-2026-126070: Cross-Site Scripting in Ruby on Rails Action View strip_tags Helper

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.

Amit Schendel
Amit Schendel
4 views•10 min read
•about 7 hours ago•CVE-2026-126078
6.1

CVE-2026-126078: Cross-Site Scripting in Ruby on Rails Action View highlight Helper

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.

Amit Schendel
Amit Schendel
4 views•9 min read