-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Add STRIDE threat model to security docs #9562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
9f24881
b71b4b9
b300e78
0c0bdf8
07b20b3
74e07b5
13433dc
2911422
114e4d5
1f02641
5af49b3
d3b73ea
da06640
0cb00ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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). | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,3 +8,4 @@ Handbook | |
| tutorial | ||
| concepts | ||
| appendices | ||
| security | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| <https://en.wikipedia.org/wiki/STRIDE_(security)>`_ framework and covers the | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
| 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) | ||
|
radarhere marked this conversation as resolved.
Outdated
|
||
| ▼ | ||
| ┌──────────────────────────────────────────┐ | ||
| │ C libraries (bundled or system) │ | ||
| │ libjpeg · libpng · libtiff · libwebp │ | ||
| │ openjpeg · freetype · littlecms │ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to be a little picky and suggest littlecms2, rather than littlecms. If this is intended to be a complete list, then it is missing libimagequant, libraqm, harfbuzz, fribidi, libavif, libxcb and zlib(-ng). If it's not intended to be a complete list, then I question if libpng should here - it's something that's used by other dependencies, not by used by us directly, so it's not a top tier dependency.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if it's intended to be a complete list and is littlecms2 a newer version of littlecms?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's go with not intended to be a complete list. |
||
| └──────────────────────────────────────────┘ | ||
|
|
||
| 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. | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
|
|
||
| **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 | ||
|
radarhere marked this conversation as resolved.
Outdated
|
||
| ``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 | ||
|
radarhere marked this conversation as resolved.
Outdated
|
||
| 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. | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
|
|
||
| **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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, this is not a complete list. |
||
| ship malicious binaries. | ||
|
|
||
| *Mitigations:* pin with hash verification (``pip install --require-hashes``); | ||
| monitor `Pillow security advisories | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
| <https://github.com/python-pillow/Pillow/security/advisories>`_; use | ||
| Dependabot or OSV-Scanner for bundled C library CVEs. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this document aimed at users? This is mitigation advice for us. |
||
|
|
||
| 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``. | ||
|
radarhere marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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. | ||
|
radarhere marked this conversation as resolved.
Outdated
|
||
|
|
||
| *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 | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
| separately capped by ``PngImagePlugin.MAX_TEXT_CHUNK`` (1 MiB) and | ||
| ``MAX_TEXT_MEMORY`` (64 MiB). | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
|
|
||
| *Mitigations:* **never** set ``Image.MAX_IMAGE_PIXELS = None`` in production; | ||
| treat ``DecompressionBombWarning`` as an error; set OS/container memory limits | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
| 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) | ||
|
|
||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
| **E-3 — ``ImageMath.unsafe_eval()`` code injection** | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
|
|
||
| :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 | ||
|
radarhere marked this conversation as resolved.
Outdated
|
||
| advisories <https://github.com/python-pillow/Pillow/security/advisories>`_. | ||
| 5. **Enforce** ``MAX_IMAGE_PIXELS`` — never set it to ``None``; treat | ||
| ``DecompressionBombWarning`` as an error. | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
| 6. **Allowlist image formats** — unregister plugins your application does not | ||
| need. | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
| 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``, | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
| and all exceptions from ``Image.open()``. | ||
|
|
||
| .. _security-reporting: | ||
|
|
||
| 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 | ||
|
aclark4life marked this conversation as resolved.
Outdated
|
||
| disclosure. | ||
|
|
||
| **Do not report sensitive vulnerability information in public.** | ||
Uh oh!
There was an error while loading. Please reload this page.