From 9f24881521e64310a9f888c66a8310599d20cac5 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 14 Apr 2026 12:13:45 -0400 Subject: [PATCH 01/14] Add STRIDE threat model to security docs - Update .github/SECURITY.md with threat model summary and link to handbook - Add docs/handbook/security.rst with full STRIDE analysis (14 threats across Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, and Elevation of Privilege categories) - Add prioritised mitigation recommendations - Link security.rst into the handbook toctree Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/SECURITY.md | 16 ++- docs/handbook/index.rst | 1 + docs/handbook/security.rst | 261 +++++++++++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 docs/handbook/security.rst diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 42ff1615b9e..2b668cc55b4 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,7 +1,21 @@ # Security policy +## Reporting a vulnerability + To report sensitive vulnerability information, report it [privately on GitHub](https://github.com/python-pillow/Pillow/security/advisories/new). If you cannot use GitHub, use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. -DO NOT report sensitive vulnerability information in public. +**DO NOT report sensitive vulnerability information in public.** + +## Threat model + +Pillow's primary attack surface is parsing untrusted image data. A full STRIDE threat model covering spoofing, tampering, repudiation, information disclosure, denial of service, and elevation of privilege is maintained in the [Security handbook page](https://pillow.readthedocs.io/en/stable/handbook/security.html). + +Key risks to be aware of when using Pillow to process untrusted images: + +- **Decompression bombs** — do not set `Image.MAX_IMAGE_PIXELS = None` in production. +- **EPS files invoke Ghostscript** — block EPS input at the application layer unless strictly required. +- **`ImageMath.unsafe_eval()`** — never pass user-controlled strings to this function; use `lambda_eval` instead. +- **C extension memory safety** — keep Pillow and its bundled C libraries (libjpeg, libpng, libtiff, libwebp, etc.) up to date. +- **Sandboxing** — for high-risk deployments, run image processing in a sandboxed subprocess. diff --git a/docs/handbook/index.rst b/docs/handbook/index.rst index acdeff7db3a..23255eb8ac0 100644 --- a/docs/handbook/index.rst +++ b/docs/handbook/index.rst @@ -8,3 +8,4 @@ Handbook tutorial concepts appendices + security diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst new file mode 100644 index 00000000000..045b9410a08 --- /dev/null +++ b/docs/handbook/security.rst @@ -0,0 +1,261 @@ +Security +======== + +Pillow's primary attack surface is **parsing untrusted image data**. This page +documents the threat model for developers integrating Pillow into applications +that handle images from untrusted sources, along with recommended mitigations. + +To report a vulnerability see :ref:`security-reporting`. + +.. _security-threat-model: + +Threat model (STRIDE) +--------------------- + +The analysis below follows the `STRIDE +`_ framework and covers the +boundary between untrusted image input and the Pillow API. + +.. code-block:: text + + ┌──────────────────────────────────────────┐ + Untrusted zone │ Pillow API │ + ───────────── │ │ + Image files ────►│ Image.open() ──► Format plugins │ + Byte streams │ (40+ parsers) (Python + C FFI) │ + User metadata │ │ + │ ImageMath.unsafe_eval(expr) ───────────┼──► Python eval() + │ ImageShow.show(image) ─────────────────┼──► os.system / subprocess + │ EpsImagePlugin.open(eps) ──────────────┼──► Ghostscript (gs) + └──────────────┬───────────────────────────┘ + │ C extension (_imaging) + ▼ + ┌──────────────────────────────────────────┐ + │ C libraries (bundled or system) │ + │ libjpeg · libpng · libtiff · libwebp │ + │ openjpeg · freetype · littlecms │ + └──────────────────────────────────────────┘ + +Spoofing +^^^^^^^^ + +**S-1 — Format sniffing bypass** + +``Image.open()`` detects format by magic bytes, not file extension or MIME +type. An attacker can name a file ``safe.png`` while its content is TIFF, JPEG +2000, or EPS, causing a different — potentially more dangerous — parser to run. + +*Mitigations:* validate MIME type and magic bytes independently before calling +``Image.open()``; pass the ``format`` parameter explicitly; maintain an +allowlist of accepted formats. + +**S-2 — Plugin registry spoofing** + +Pillow's format registry is a global mutable dictionary. A malicious package +installed in the same environment could register a replacement parser for a +well-known format. + +*Mitigations:* use isolated virtual environments with pinned, hash-verified +dependencies; audit ``Image.registered_extensions()`` at startup. + +Tampering +^^^^^^^^^ + +**T-1 — Malicious metadata propagation** + +Pillow preserves EXIF, XMP, IPTC, ICC profiles, and comments when +round-tripping images. Applications that store or render metadata without +sanitisation are vulnerable to second-order injection (SQLi, XSS, command +injection). + +*Mitigations:* treat all values from ``image.info``, ``image._getexif()``, and +``image.text`` as untrusted; sanitise before storing or rendering; strip +metadata when it is not required. + +**T-2 — Covert data channel (steganography)** + +Pillow does not remove hidden data (JPEG comments, PNG text chunks, appended +bytes) when re-saving. An attacker can embed data that survives the +encode-decode cycle invisibly. + +*Mitigations:* to guarantee a clean output, load pixel data via +``image.tobytes()`` and rebuild the image from raw bytes before saving. + +**T-3 — Supply chain tampering** + +Pre-compiled wheels bundle libjpeg-turbo, libpng, libtiff, libwebp, openjpeg, +freetype, and littlecms. A compromised PyPI release or build pipeline could +ship malicious binaries. + +*Mitigations:* pin with hash verification (``pip install --require-hashes``); +monitor `Pillow security advisories +`_; use +Dependabot or OSV-Scanner for bundled C library CVEs. + +Repudiation +^^^^^^^^^^^ + +**R-1 — No structured audit trail** + +Pillow does not emit structured audit logs of files opened, formats detected, +or operations performed, making forensic investigation harder after an +incident. + +*Mitigations:* applications should log the filename/hash, detected format, and +dimensions of every image processed; log and alert on +``Image.DecompressionBombWarning`` and ``PIL.UnidentifiedImageError``. + +Information disclosure +^^^^^^^^^^^^^^^^^^^^^^ + +**I-1 — Metadata in saved images** + +GPS coordinates, author names, software version strings, and ICC profiles can +be inadvertently included in output images served publicly. + +*Mitigations:* explicitly strip EXIF and XMP on save (set ``exif=b""``, +``icc_profile=None``, omit ``pnginfo``); verify output with ``exiftool`` in CI. + +**I-2 — Sensitive exception messages** + +Parser errors can include byte offsets, dimension values, and tile descriptors. +Propagating these to API responses aids attacker reconnaissance. + +*Mitigations:* catch ``PIL.UnidentifiedImageError``, +``PIL.Image.DecompressionBombError``, and general exceptions at the +application boundary; return generic messages to clients. + +**I-3 — Temporary file exposure** + +Several code paths write pixel data to temporary files via +``tempfile.mkstemp()``. Exception paths can leave these files behind on shared +filesystems. + +*Mitigations:* files are created with mode ``0o600``; mount ``/tmp`` as a +per-container ``tmpfs``; ensure ``try/finally`` cleanup is in place. + +Denial of service +^^^^^^^^^^^^^^^^^ + +**D-1 — Decompression bomb** + +A small compressed image can expand to gigabytes in memory. +:py:data:`PIL.Image.MAX_IMAGE_PIXELS` (~89 MP by default) raises +``DecompressionBombError`` at 2× the limit and +``DecompressionBombWarning`` at 1×. PNG text chunks are +separately capped by ``PngImagePlugin.MAX_TEXT_CHUNK`` (1 MiB) and +``MAX_TEXT_MEMORY`` (64 MiB). + +*Mitigations:* **never** set ``Image.MAX_IMAGE_PIXELS = None`` in production; +treat ``DecompressionBombWarning`` as an error; set OS/container memory limits +per worker. + +**D-2 — CPU exhaustion** + +Large-but-legal images (within ``MAX_IMAGE_PIXELS``) can still saturate CPU +through high-quality resampling, convolution filters, or complex draw +operations. + +*Mitigations:* apply per-request CPU time limits; set a practical dimension +ceiling below ``MAX_IMAGE_PIXELS``; rate-limit processing requests. + +**D-3 — Algorithmic complexity in parsers** + +Formats such as TIFF (nested IFD chains), animated GIF/WebP (many frames), and +PNG (many text chunks) can exhaust CPU or memory before pixel data is decoded. + +*Mitigations:* restrict accepted formats to the minimum required; enforce a +file-size limit before passing data to Pillow; use per-request timeouts. + +Elevation of privilege +^^^^^^^^^^^^^^^^^^^^^^ + +**E-1 — C extension memory corruption (RCE)** + +Pillow's ~87 C source files and its bundled C libraries process +attacker-controlled bytes. Historical CVEs include buffer overflows, integer +overflows, and use-after-free vulnerabilities that allow arbitrary code +execution. + +*Mitigations:* keep Pillow and all C libraries up to date; compile with +hardening flags (ASLR, stack canaries, PIE, ``_FORTIFY_SOURCE=2``); run image +processing in a sandboxed subprocess (seccomp-bpf, AppArmor, or a restricted +container). + +**E-2 — Ghostscript exploitation via EPS (RCE)** + +Opening an EPS file invokes the system Ghostscript binary (``gs``) via +``subprocess``. Ghostscript has a long history of sandbox-escape CVEs +permitting arbitrary code execution from malicious PostScript. + +*Mitigations:* **block EPS files** at the application input layer; if EPS must +be supported, run Ghostscript in a fully isolated sandbox with no network and +no sensitive mounts; unregister the plugin if unused:: + + from PIL import Image, EpsImagePlugin + Image.OPEN.pop("EPS", None) + +**E-3 — ``ImageMath.unsafe_eval()`` code injection** + +:py:meth:`~PIL.ImageMath.unsafe_eval` calls Python's built-in ``eval()`` with +only a minimal ``__builtins__`` restriction, which can be bypassed via +introspection. Any user-controlled string passed to this function results in +arbitrary code execution. + +*Mitigations:* **never** pass user-controlled strings to +``ImageMath.unsafe_eval()``; use :py:meth:`~PIL.ImageMath.lambda_eval` instead, +which accepts a Python callable and never calls ``eval``. + +**E-4 — Font path traversal via ``ImageFont``** + +``ImageFont.truetype(font, size)`` passes the filename to the FreeType C +library. If font paths are constructed from user input without +canonicalisation, an attacker may supply a path like +``../../../../etc/passwd``. + +*Mitigations:* never construct font paths from user input; if font selection +must be user-driven, resolve names against an explicit allowlist of +pre-validated absolute paths. + +.. _security-recommendations: + +Recommendations +--------------- + +The following mitigations are listed in priority order. + +1. **Sandbox image processing** — run Pillow workers in a seccomp/AppArmor- + restricted subprocess, isolated from the main application process. +2. **Block or sandbox EPS** — reject EPS at the application boundary, or run + Ghostscript in an isolated container. +3. **Never use** ``ImageMath.unsafe_eval()`` **with user input** — migrate all + callers to :py:meth:`~PIL.ImageMath.lambda_eval`. +4. **Keep all dependencies current** — Pillow, libjpeg, libpng, libtiff, + libwebp, openjpeg, freetype, Ghostscript. Subscribe to `Pillow security + advisories `_. +5. **Enforce** ``MAX_IMAGE_PIXELS`` — never set it to ``None``; treat + ``DecompressionBombWarning`` as an error. +6. **Allowlist image formats** — unregister plugins your application does not + need. +7. **Strip metadata on output** — never pass through EXIF/XMP/ICC from user + uploads to publicly served images. +8. **Sanitise all metadata** returned by Pillow before using it downstream. +9. **Pin dependencies with hash verification** — use + ``pip install --require-hashes`` and lockfiles. +10. **Log and alert** on ``DecompressionBombWarning``, + ``DecompressionBombError``, ``PIL.UnidentifiedImageError``, + and all exceptions from ``Image.open()``. + +.. _security-reporting: + +Reporting a vulnerability +------------------------- + +To report sensitive vulnerability information, report it `privately on GitHub +`_. + +If you cannot use GitHub, use the `Tidelift security contact +`_. Tidelift will coordinate the fix and +disclosure. + +**Do not report sensitive vulnerability information in public.** From b71b4b98d900d3a8450577f49a37818c03b4a7bb Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 14 Apr 2026 19:56:59 -0400 Subject: [PATCH 02/14] Lint --- docs/handbook/security.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index 045b9410a08..ebff8199f7f 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -224,7 +224,7 @@ Recommendations The following mitigations are listed in priority order. -1. **Sandbox image processing** — run Pillow workers in a seccomp/AppArmor- +1. **Sandbox image processing** — run Pillow workers in a seccomp/AppArmor restricted subprocess, isolated from the main application process. 2. **Block or sandbox EPS** — reject EPS at the application boundary, or run Ghostscript in an isolated container. From b300e788384e8fff6ac1fcc5f877599ebc24148a Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 14 Apr 2026 20:08:05 -0400 Subject: [PATCH 03/14] Update docs/handbook/security.rst Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/handbook/security.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index ebff8199f7f..dc7e96c605c 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -140,11 +140,12 @@ Denial of service **D-1 — Decompression bomb** A small compressed image can expand to gigabytes in memory. -:py:data:`PIL.Image.MAX_IMAGE_PIXELS` (~89 MP by default) raises +:py:data:`PIL.Image.MAX_IMAGE_PIXELS` raises ``DecompressionBombError`` at 2× the limit and ``DecompressionBombWarning`` at 1×. PNG text chunks are -separately capped by ``PngImagePlugin.MAX_TEXT_CHUNK`` (1 MiB) and -``MAX_TEXT_MEMORY`` (64 MiB). +separately capped by ``PngImagePlugin.MAX_TEXT_CHUNK`` and +``MAX_TEXT_MEMORY``. Check the values in your installed Pillow version at +runtime or in the reference/source for the current defaults. *Mitigations:* **never** set ``Image.MAX_IMAGE_PIXELS = None`` in production; treat ``DecompressionBombWarning`` as an error; set OS/container memory limits @@ -188,13 +189,12 @@ Opening an EPS file invokes the system Ghostscript binary (``gs``) via ``subprocess``. Ghostscript has a long history of sandbox-escape CVEs permitting arbitrary code execution from malicious PostScript. -*Mitigations:* **block EPS files** at the application input layer; if EPS must -be supported, run Ghostscript in a fully isolated sandbox with no network and -no sensitive mounts; unregister the plugin if unused:: - - from PIL import Image, EpsImagePlugin - Image.OPEN.pop("EPS", None) - +*Mitigations:* **block EPS files** at the application input layer before +passing files to Pillow; if EPS must be supported, run Ghostscript in a fully +isolated sandbox with no network and no sensitive mounts. Pillow does not +provide a stable public API for unregistering individual format plugins, so do +not rely on mutating internal registries such as ``Image.OPEN`` as a security +control. **E-3 — ``ImageMath.unsafe_eval()`` code injection** :py:meth:`~PIL.ImageMath.unsafe_eval` calls Python's built-in ``eval()`` with From 0c0bdf8d5adadb5e28d08246936e89ccfe77b020 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 15 Apr 2026 13:03:19 -0400 Subject: [PATCH 04/14] Update security docs - docs/handbook/security.rst - .github/SECURITY.md Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/SECURITY.md | 4 ++-- docs/handbook/security.rst | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 2b668cc55b4..c9a396aa86c 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -4,13 +4,13 @@ To report sensitive vulnerability information, report it [privately on GitHub](https://github.com/python-pillow/Pillow/security/advisories/new). -If you cannot use GitHub, use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. +If you cannot use GitHub, use the [Tidelift security contact](https://tidelift.com/docs/security). Tidelift will coordinate the fix and disclosure. **DO NOT report sensitive vulnerability information in public.** ## Threat model -Pillow's primary attack surface is parsing untrusted image data. A full STRIDE threat model covering spoofing, tampering, repudiation, information disclosure, denial of service, and elevation of privilege is maintained in the [Security handbook page](https://pillow.readthedocs.io/en/stable/handbook/security.html). +Pillow's primary attack surface is parsing untrusted image data. A full STRIDE threat model covering spoofing, tampering, repudiation, information disclosure, denial of service, and elevation of privilege is maintained in the [Security handbook page](https://pillow.readthedocs.io/en/latest/handbook/security.html). Key risks to be aware of when using Pillow to process untrusted images: diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index dc7e96c605c..e018f099ae8 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -141,14 +141,14 @@ Denial of service A small compressed image can expand to gigabytes in memory. :py:data:`PIL.Image.MAX_IMAGE_PIXELS` raises -``DecompressionBombError`` at 2× the limit and -``DecompressionBombWarning`` at 1×. PNG text chunks are +``Image.DecompressionBombError`` at 2× the limit and +``Image.DecompressionBombWarning`` at 1×. PNG text chunks are separately capped by ``PngImagePlugin.MAX_TEXT_CHUNK`` and ``MAX_TEXT_MEMORY``. Check the values in your installed Pillow version at runtime or in the reference/source for the current defaults. *Mitigations:* **never** set ``Image.MAX_IMAGE_PIXELS = None`` in production; -treat ``DecompressionBombWarning`` as an error; set OS/container memory limits +treat ``Image.DecompressionBombWarning`` as an error; set OS/container memory limits per worker. **D-2 — CPU exhaustion** @@ -234,7 +234,7 @@ The following mitigations are listed in priority order. libwebp, openjpeg, freetype, Ghostscript. Subscribe to `Pillow security advisories `_. 5. **Enforce** ``MAX_IMAGE_PIXELS`` — never set it to ``None``; treat - ``DecompressionBombWarning`` as an error. + ``Image.DecompressionBombWarning`` as an error. 6. **Allowlist image formats** — unregister plugins your application does not need. 7. **Strip metadata on output** — never pass through EXIF/XMP/ICC from user @@ -242,8 +242,8 @@ The following mitigations are listed in priority order. 8. **Sanitise all metadata** returned by Pillow before using it downstream. 9. **Pin dependencies with hash verification** — use ``pip install --require-hashes`` and lockfiles. -10. **Log and alert** on ``DecompressionBombWarning``, - ``DecompressionBombError``, ``PIL.UnidentifiedImageError``, +10. **Log and alert** on ``Image.DecompressionBombWarning``, + ``Image.DecompressionBombError``, ``PIL.UnidentifiedImageError``, and all exceptions from ``Image.open()``. .. _security-reporting: @@ -255,7 +255,7 @@ To report sensitive vulnerability information, report it `privately on GitHub `_. If you cannot use GitHub, use the `Tidelift security contact -`_. Tidelift will coordinate the fix and +`_. Tidelift will coordinate the fix and disclosure. **Do not report sensitive vulnerability information in public.** From 07b20b3b33a961c87e93559454b1cc1d9a131b62 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 16 Apr 2026 06:45:55 -0400 Subject: [PATCH 05/14] Remove Sensitive exception messages --- docs/handbook/security.rst | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index e018f099ae8..ec91ea82c72 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -116,16 +116,7 @@ be inadvertently included in output images served publicly. *Mitigations:* explicitly strip EXIF and XMP on save (set ``exif=b""``, ``icc_profile=None``, omit ``pnginfo``); verify output with ``exiftool`` in CI. -**I-2 — Sensitive exception messages** - -Parser errors can include byte offsets, dimension values, and tile descriptors. -Propagating these to API responses aids attacker reconnaissance. - -*Mitigations:* catch ``PIL.UnidentifiedImageError``, -``PIL.Image.DecompressionBombError``, and general exceptions at the -application boundary; return generic messages to clients. - -**I-3 — Temporary file exposure** +**I-2 — Temporary file exposure** Several code paths write pixel data to temporary files via ``tempfile.mkstemp()``. Exception paths can leave these files behind on shared From 74e07b5b8adbc1fe5665ae364a40033ee546ccbb Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 16 Apr 2026 06:48:09 -0400 Subject: [PATCH 06/14] Lint --- docs/handbook/security.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index ec91ea82c72..2984d8e2bbb 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -22,7 +22,7 @@ boundary between untrusted image input and the Pillow API. Untrusted zone │ Pillow API │ ───────────── │ │ Image files ────►│ Image.open() ──► Format plugins │ - Byte streams │ (40+ parsers) (Python + C FFI) │ + Byte streams │ (40+ parsers) (Python + C FFI) │ User metadata │ │ │ ImageMath.unsafe_eval(expr) ───────────┼──► Python eval() │ ImageShow.show(image) ─────────────────┼──► os.system / subprocess From 13433dc0a9c86abb338a01439af3f3795b2e3994 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 21 Apr 2026 11:07:58 -0400 Subject: [PATCH 07/14] Update docs/handbook/security.rst Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/handbook/security.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index 2984d8e2bbb..58d066a1d14 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -46,8 +46,8 @@ type. An attacker can name a file ``safe.png`` while its content is TIFF, JPEG 2000, or EPS, causing a different — potentially more dangerous — parser to run. *Mitigations:* validate MIME type and magic bytes independently before calling -``Image.open()``; pass the ``format`` parameter explicitly; maintain an -allowlist of accepted formats. +``Image.open()``; pass the ``formats`` argument with an allowlist of accepted +formats. **S-2 — Plugin registry spoofing** @@ -226,8 +226,9 @@ The following mitigations are listed in priority order. advisories `_. 5. **Enforce** ``MAX_IMAGE_PIXELS`` — never set it to ``None``; treat ``Image.DecompressionBombWarning`` as an error. -6. **Allowlist image formats** — unregister plugins your application does not - need. +6. **Allowlist image formats** — restrict accepted formats when opening + images, for example with ``Image.open(..., formats=...)``, and isolate + installs/environments if you need to minimise supported formats. 7. **Strip metadata on output** — never pass through EXIF/XMP/ICC from user uploads to publicly served images. 8. **Sanitise all metadata** returned by Pillow before using it downstream. From 291142275383c4455f9bcdd1a9041cb2e60d48e1 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 21 Apr 2026 11:11:00 -0400 Subject: [PATCH 08/14] s/littlecms/littlecms2/ --- docs/handbook/security.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index 58d066a1d14..56333f70f2f 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -33,7 +33,7 @@ boundary between untrusted image input and the Pillow API. ┌──────────────────────────────────────────┐ │ C libraries (bundled or system) │ │ libjpeg · libpng · libtiff · libwebp │ - │ openjpeg · freetype · littlecms │ + │ openjpeg · freetype · littlecms2 │ └──────────────────────────────────────────┘ Spoofing @@ -84,7 +84,7 @@ encode-decode cycle invisibly. **T-3 — Supply chain tampering** Pre-compiled wheels bundle libjpeg-turbo, libpng, libtiff, libwebp, openjpeg, -freetype, and littlecms. A compromised PyPI release or build pipeline could +freetype, and littlecms2. A compromised PyPI release or build pipeline could ship malicious binaries. *Mitigations:* pin with hash verification (``pip install --require-hashes``); From 114e4d5695308a6c33791f011e4024dadc5b010e Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 21 Apr 2026 11:22:58 -0400 Subject: [PATCH 09/14] docs: list all 8 C extensions in security threat model diagram Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/handbook/security.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index 56333f70f2f..c3e692a9a9c 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -28,7 +28,10 @@ boundary between untrusted image input and the Pillow API. │ ImageShow.show(image) ─────────────────┼──► os.system / subprocess │ EpsImagePlugin.open(eps) ──────────────┼──► Ghostscript (gs) └──────────────┬───────────────────────────┘ - │ C extension (_imaging) + │ C extensions: + │ _imaging · _imagingft · _imagingcms + │ _webp · _avif · _imagingtk + │ _imagingmath · _imagingmorph ▼ ┌──────────────────────────────────────────┐ │ C libraries (bundled or system) │ From 1f026416f9911614f0cc938bfde1197383c0cec9 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 21 Apr 2026 11:23:54 -0400 Subject: [PATCH 10/14] Update docs/handbook/security.rst Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/handbook/security.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index c3e692a9a9c..dad1e4a292b 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -81,8 +81,8 @@ Pillow does not remove hidden data (JPEG comments, PNG text chunks, appended bytes) when re-saving. An attacker can embed data that survives the encode-decode cycle invisibly. -*Mitigations:* to guarantee a clean output, load pixel data via -``image.tobytes()`` and rebuild the image from raw bytes before saving. +*Mitigations:* to guarantee a clean output when saving, create a new image instance via +``image.copy()`` and delete the ``image.info`` contents. **T-3 — Supply chain tampering** From 5af49b380e103e0045a0317437fe853af20f8fbe Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 21 Apr 2026 11:32:36 -0400 Subject: [PATCH 11/14] docs: address Andrew's review comments on security.rst - Add image.getexif() alongside image._getexif() in T-1 mitigations - Remove 'appended bytes' from T-2 (Pillow does not preserve them on resave) - Reframe R-1 threat as user-facing (not Pillow dev advice); add DecompressionBombError to the log/alert list - Add blank line before E-3 heading - Qualify dependency list in recommendation #4 as non-exhaustive Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/handbook/security.rst | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index dad1e4a292b..34ce3e30f63 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -71,14 +71,14 @@ round-tripping images. Applications that store or render metadata without sanitisation are vulnerable to second-order injection (SQLi, XSS, command injection). -*Mitigations:* treat all values from ``image.info``, ``image._getexif()``, and -``image.text`` as untrusted; sanitise before storing or rendering; strip -metadata when it is not required. +*Mitigations:* treat all values from ``image.info``, ``image._getexif()``, +``image.getexif()``, and ``image.text`` as untrusted; sanitise before storing +or rendering; strip metadata when it is not required. **T-2 — Covert data channel (steganography)** -Pillow does not remove hidden data (JPEG comments, PNG text chunks, appended -bytes) when re-saving. An attacker can embed data that survives the +Pillow does not remove hidden data (JPEG comments, PNG text chunks) when +re-saving. An attacker can embed data that survives the encode-decode cycle invisibly. *Mitigations:* to guarantee a clean output when saving, create a new image instance via @@ -100,13 +100,13 @@ Repudiation **R-1 — No structured audit trail** -Pillow does not emit structured audit logs of files opened, formats detected, -or operations performed, making forensic investigation harder after an -incident. +Without application-level logging there is no record of which images were +opened, what formats were detected, or what operations were performed, making +forensic investigation harder after an incident. -*Mitigations:* applications should log the filename/hash, detected format, and -dimensions of every image processed; log and alert on -``Image.DecompressionBombWarning`` and ``PIL.UnidentifiedImageError``. +*Mitigations:* log the filename/hash, detected format, and dimensions of every +image processed; log and alert on ``Image.DecompressionBombWarning``, +``Image.DecompressionBombError``, and ``PIL.UnidentifiedImageError``. Information disclosure ^^^^^^^^^^^^^^^^^^^^^^ @@ -189,6 +189,7 @@ isolated sandbox with no network and no sensitive mounts. Pillow does not provide a stable public API for unregistering individual format plugins, so do not rely on mutating internal registries such as ``Image.OPEN`` as a security control. + **E-3 — ``ImageMath.unsafe_eval()`` code injection** :py:meth:`~PIL.ImageMath.unsafe_eval` calls Python's built-in ``eval()`` with @@ -224,8 +225,9 @@ The following mitigations are listed in priority order. Ghostscript in an isolated container. 3. **Never use** ``ImageMath.unsafe_eval()`` **with user input** — migrate all callers to :py:meth:`~PIL.ImageMath.lambda_eval`. -4. **Keep all dependencies current** — Pillow, libjpeg, libpng, libtiff, - libwebp, openjpeg, freetype, Ghostscript. Subscribe to `Pillow security +4. **Keep all dependencies current** — Pillow and its C library dependencies + (including libjpeg, libpng, libtiff, libwebp, openjpeg, freetype, + littlecms2, Ghostscript, and others). Subscribe to `Pillow security advisories `_. 5. **Enforce** ``MAX_IMAGE_PIXELS`` — never set it to ``None``; treat ``Image.DecompressionBombWarning`` as an error. From d3b73ea4628368e369bf653ca80c36a61ce485aa Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 21 Apr 2026 11:33:48 -0400 Subject: [PATCH 12/14] Update docs/handbook/security.rst Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/handbook/security.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index 34ce3e30f63..208afb287e0 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -190,6 +190,7 @@ provide a stable public API for unregistering individual format plugins, so do not rely on mutating internal registries such as ``Image.OPEN`` as a security control. + **E-3 — ``ImageMath.unsafe_eval()`` code injection** :py:meth:`~PIL.ImageMath.unsafe_eval` calls Python's built-in ``eval()`` with From da0664087309ece64410a44d3fb55851a2fafd8c Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 21 Apr 2026 11:58:06 -0400 Subject: [PATCH 13/14] docs: fix nested inline markup in E-3 and E-4 headings RST does not allow inline markup (backticks) nested inside bold markers. Remove backticks from the E-3 and E-4 heading text so they render correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/handbook/security.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index 208afb287e0..6046466e822 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -191,7 +191,7 @@ not rely on mutating internal registries such as ``Image.OPEN`` as a security control. -**E-3 — ``ImageMath.unsafe_eval()`` code injection** +**E-3 — ImageMath.unsafe_eval() code injection** :py:meth:`~PIL.ImageMath.unsafe_eval` calls Python's built-in ``eval()`` with only a minimal ``__builtins__`` restriction, which can be bypassed via @@ -202,7 +202,7 @@ arbitrary code execution. ``ImageMath.unsafe_eval()``; use :py:meth:`~PIL.ImageMath.lambda_eval` instead, which accepts a Python callable and never calls ``eval``. -**E-4 — Font path traversal via ``ImageFont``** +**E-4 — Font path traversal via ImageFont** ``ImageFont.truetype(font, size)`` passes the filename to the FreeType C library. If font paths are constructed from user input without From 0cb00acc921fcb236b0569dbf45ea9e51bbcaa1a Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 22 Apr 2026 07:34:40 -0400 Subject: [PATCH 14/14] Update docs/handbook/security.rst Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/handbook/security.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst index 6046466e822..c13389134ff 100644 --- a/docs/handbook/security.rst +++ b/docs/handbook/security.rst @@ -13,7 +13,7 @@ Threat model (STRIDE) --------------------- The analysis below follows the `STRIDE -`_ framework and covers the +`_ framework and covers the boundary between untrusted image input and the Pillow API. .. code-block:: text @@ -22,7 +22,7 @@ boundary between untrusted image input and the Pillow API. Untrusted zone │ Pillow API │ ───────────── │ │ Image files ────►│ Image.open() ──► Format plugins │ - Byte streams │ (40+ parsers) (Python + C FFI) │ + Byte streams │ (40+ parsers) (Python + C FFI) │ User metadata │ │ │ ImageMath.unsafe_eval(expr) ───────────┼──► Python eval() │ ImageShow.show(image) ─────────────────┼──► os.system / subprocess @@ -87,11 +87,11 @@ encode-decode cycle invisibly. **T-3 — Supply chain tampering** Pre-compiled wheels bundle libjpeg-turbo, libpng, libtiff, libwebp, openjpeg, -freetype, and littlecms2. A compromised PyPI release or build pipeline could -ship malicious binaries. +freetype, littlecms2, and other libraries. A compromised PyPI release or build pipeline +could ship malicious binaries. -*Mitigations:* pin with hash verification (``pip install --require-hashes``); -monitor `Pillow security advisories +*Mitigations:* pin with hash verification +(``python3 -m pip install --require-hashes``); monitor `Pillow security advisories `_; use Dependabot or OSV-Scanner for bundled C library CVEs.