From f708c005276b126bd2a08b6f383fcc58ac299eb9 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Fri, 13 Feb 2026 19:38:48 -0800 Subject: [PATCH 01/83] Fix UnboundLocalError in _border for invalid tuple lengths and document rgba() color format The _border helper in ImageOps raised UnboundLocalError when given a tuple with a length other than 2 or 4 (e.g. 1-tuple or 3-tuple). This changes it to raise a clear ValueError instead. Also adds documentation for the rgba() color format in ImageColor, which was supported in code and tested but missing from the docs. --- Tests/test_imageops.py | 7 +++++++ docs/reference/ImageColor.rst | 4 ++++ src/PIL/ImageOps.py | 3 +++ 3 files changed, 14 insertions(+) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 35fe3bb8a..31b7abecd 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -256,6 +256,13 @@ def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: assert_image_equal(im_cropped, im) +@pytest.mark.parametrize("border", ((1,), (1, 2, 3), (1, 2, 3, 4, 5))) +def test_expand_invalid_border(border: tuple[int, ...]) -> None: + im = Image.new("1", (1, 1)) + with pytest.raises(ValueError): + ImageOps.expand(im, border) + + def test_colorize_2color() -> None: # Test the colorizing function with 2-color functionality diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst index 68e228dba..82bd359bc 100644 --- a/docs/reference/ImageColor.rst +++ b/docs/reference/ImageColor.rst @@ -27,6 +27,10 @@ The ImageColor module supports the following string formats: as three percentages (0% to 100%). For example, ``rgb(255,0,0)`` and ``rgb(100%,0%,0%)`` both specify pure red. +* RGBA functions, given as ``rgba(red, green, blue, alpha)`` where the color + values and the alpha value are integers in the range 0 to 255. For example, + ``rgba(255,0,0,128)`` specifies pure red with 50% opacity. + * Hue-Saturation-Lightness (HSL) functions, given as ``hsl(hue, saturation%, lightness%)`` where hue is the color given as an angle between 0 and 360 (red=0, green=120, blue=240), saturation is a value between 0% and 100% diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 42b10bd7b..8fdb9564d 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -36,6 +36,9 @@ def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: left, top = right, bottom = border elif len(border) == 4: left, top, right, bottom = border + else: + msg = "border must be an integer or a 2- or 4-tuple" + raise ValueError(msg) else: left = top = right = bottom = border return left, top, right, bottom From e50d8a51923aca66bc573c476b599d0072bfc158 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sun, 22 Feb 2026 18:50:14 -0800 Subject: [PATCH 02/83] Improve border validation error message wording --- src/PIL/ImageOps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 8fdb9564d..f0ae142b9 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -37,7 +37,7 @@ def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: elif len(border) == 4: left, top, right, bottom = border else: - msg = "border must be an integer or a 2- or 4-tuple" + msg = "border must be an integer, or a tuple of two or four elements" raise ValueError(msg) else: left = top = right = bottom = border From 7f68decf2c34ad4bc2a700be8410df679180b8d9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 Apr 2026 22:16:51 +1100 Subject: [PATCH 03/83] Clarified condition --- src/PIL/PngImagePlugin.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 76a15bd0d..9bfeb1104 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1443,9 +1443,9 @@ def _save( palette_bytes += b"\0" chunk(fp, b"PLTE", palette_bytes) - transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None)) + transparency = im.encoderinfo.get("transparency", im.info.get("transparency")) - if transparency or transparency == 0: + if transparency is not None: if im.mode == "P": # limit to actual palette size alpha_bytes = colors @@ -1461,17 +1461,15 @@ def _save( elif im.mode == "RGB": red, green, blue = transparency chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue)) - else: - if "transparency" in im.encoderinfo: - # don't bother with transparency if it's an RGBA - # and it's in the info dict. It's probably just stale. - msg = "cannot use transparency for this mode" - raise OSError(msg) - else: - if im.mode == "P" and im.im.getpalettemode() == "RGBA": - alpha = im.im.getpalette("RGBA", "A") - alpha_bytes = colors - chunk(fp, b"tRNS", alpha[:alpha_bytes]) + elif im.encoderinfo.get("transparency") is not None: + # don't bother with transparency if it's an RGBA + # and it's in the info dict. It's probably just stale. + msg = "cannot use transparency for this mode" + raise OSError(msg) + elif im.mode == "P" and im.im.getpalettemode() == "RGBA": + alpha = im.im.getpalette("RGBA", "A") + alpha_bytes = colors + chunk(fp, b"tRNS", alpha[:alpha_bytes]) if dpi := im.encoderinfo.get("dpi"): chunk( From e58c67347a2f4eae454fa727008fa1ba4a71c923 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 Apr 2026 22:19:52 +1100 Subject: [PATCH 04/83] Raise error if transparency is incorrect type or length when saving --- Tests/test_file_png.py | 35 +++++++++++++++++++++++++++++++++-- src/PIL/PngImagePlugin.py | 24 +++++++++++++++++++----- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 3f08d1ad3..0fad0b391 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -502,8 +502,9 @@ class TestFilePng: im = roundtrip(im) assert im.info["transparency"] == (248, 248, 248) - im = roundtrip(im, transparency=(0, 1, 2)) - assert im.info["transparency"] == (0, 1, 2) + for transparency in ((0, 1, 2), [0, 1, 2]): + im = roundtrip(im, transparency=transparency) + assert im.info["transparency"] == (0, 1, 2) def test_trns_p(self, tmp_path: Path) -> None: # Check writing a transparency of 0, issue #528 @@ -518,6 +519,36 @@ class TestFilePng: assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) + def test_trns_invalid(self, tmp_path: Path) -> None: + out = tmp_path / "temp.png" + + for mode in ("1", "L", "I;16"): + im = Image.new(mode, (1, 1)) + with pytest.raises( + ValueError, match=f"transparency for {mode} must be an integer" + ): + im.save(out, transparency="invalid") + + im = Image.new("I", (1, 1)) + with pytest.warns(DeprecationWarning, match="Saving I mode images as PNG"): + with pytest.raises(ValueError): + im.save(out, transparency="invalid") + + im = Image.new("P", (1, 1)) + with pytest.raises( + ValueError, match="transparency for P must be an integer or bytes" + ): + im.save(out, transparency="invalid") + + im = Image.new("RGB", (1, 1)) + with pytest.raises( + ValueError, match="transparency for RGB must be list or tuple" + ): + im.save(out, transparency="invalid") + + with pytest.raises(ValueError, match="transparency for RGB must have length 3"): + im.save(out, transparency=(1, 2)) + def test_trns_null(self) -> None: # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 9bfeb1104..3f21fa48e 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1451,16 +1451,30 @@ def _save( alpha_bytes = colors if isinstance(transparency, bytes): chunk(fp, b"tRNS", transparency[:alpha_bytes]) - else: + elif isinstance(transparency, int): transparency = max(0, min(255, transparency)) alpha = b"\xff" * transparency + b"\0" chunk(fp, b"tRNS", alpha[:alpha_bytes]) + else: + msg = "transparency for P must be an integer or bytes" + raise ValueError(msg) elif im.mode in ("1", "L", "I", "I;16"): - transparency = max(0, min(65535, transparency)) - chunk(fp, b"tRNS", o16(transparency)) + if isinstance(transparency, int): + transparency = max(0, min(65535, transparency)) + chunk(fp, b"tRNS", o16(transparency)) + else: + msg = f"transparency for {im.mode} must be an integer" + raise ValueError(msg) elif im.mode == "RGB": - red, green, blue = transparency - chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue)) + if not isinstance(transparency, (list, tuple)): + msg = "transparency for RGB must be list or tuple" + raise ValueError(msg) + elif len(transparency) != 3: + msg = "transparency for RGB must have length 3" + raise ValueError(msg) + else: + red, green, blue = transparency + chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue)) elif im.encoderinfo.get("transparency") is not None: # don't bother with transparency if it's an RGBA # and it's in the info dict. It's probably just stale. From 117de2b181d94d2272d9b6f0a61083925e6fac6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20B=C3=A1ch?= <45133811+barttran2k@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:41:12 +0700 Subject: [PATCH 05/83] fix(security)(_imagingtk.c): unsafe pointer dereference from unchecked python i MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In `_tkinit`, `PyLong_AsVoidPtr(arg)` converts an arbitrary Python object to a `void*` pointer which is then cast to `Tcl_Interp*` and passed to `TkImaging_Init`. If `PyLong_AsVoidPtr` fails (returns NULL and sets an error), or if the caller passes an arbitrary integer value, the code proceeds to dereference it without any validation, potentially leading to a crash or arbitrary memory access. Affected files: _imagingtk.c Signed-off-by: Trần Bách <45133811+barttran2k@users.noreply.github.com> --- src/_imagingtk.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_imagingtk.c b/src/_imagingtk.c index 68d7bf4cd..7b9607cb5 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -33,8 +33,10 @@ _tkinit(PyObject *self, PyObject *args) { } interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg); + if (interp == NULL && PyErr_Occurred()) { + return NULL; + } - /* This will bomb if interp is invalid... */ TkImaging_Init(interp); Py_RETURN_NONE; From cb5736ea3e04d22b529ef75707cbc266dc7ff8f1 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 12:26:29 -0400 Subject: [PATCH 06/83] Add INCIDENT_RESPONSE.md --- .github/INCIDENT_RESPONSE.md | 441 +++++++++++++++++++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 .github/INCIDENT_RESPONSE.md diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md new file mode 100644 index 000000000..03371c4f3 --- /dev/null +++ b/.github/INCIDENT_RESPONSE.md @@ -0,0 +1,441 @@ +# Incident Response Plan — Pillow + +This document describes how the Pillow maintainers detect, triage, fix, communicate, and +learn from security incidents. It supplements the existing [Security Policy](SECURITY.md) +and [Release Checklist](../RELEASING.md). + +--- + +## 1. Preparation + +Maintaining readiness before an incident occurs reduces response time and errors under pressure. + +### 1.1 Version Support Matrix + +Only the following branches receive security fixes. Reporters should verify their affected +version before filing; maintainers should cherry-pick fixes only to supported branches. + +| Branch | Status | Notes | +|---|---|---| +| `main` | ✅ Active development | Always patched | +| Latest stable (e.g. `11.x`) | ✅ Security fixes | Current quarterly release series | +| Previous stable (e.g. `10.x`) | ⚠️ Critical only | One release series back; Critical CVEs only | +| Older branches | ❌ End of life | No security support; users must upgrade | + +> Update this table with each quarterly release. + +### 1.2 Team Readiness + +- Maintain a private list of current maintainer contact details (GitHub handles, email, + Mastodon) in a location accessible to all maintainers (e.g. a pinned private team + discussion or the Tidelift maintainer portal). +- Ensure at least two maintainers have admin access to: + - The GitHub repository (to manage Security Advisories) + - The [PyPI Pillow project](https://pypi.org/project/Pillow/) (to yank releases) + - The Tidelift maintainer portal +- Rotate and audit PyPI API tokens and GitHub Actions secrets at least once per year, + and immediately after any maintainer leaves the project. + +### 1.3 Annual Readiness Review + +Once per year (suggested: at the January quarterly release), maintainers should: + +1. Re-read this document and update any stale content (version table, contacts, tooling). +2. Verify the GitHub private security advisory flow still works (open and close a test advisory). +3. Confirm PyPI yank access is functional. +4. Review Dependabot and CodeQL alert settings are enabled on the repository. + +--- + +## 2. Scope + +This plan covers: + +| Incident type | Examples | +|---|---| +| Vulnerability in Pillow's own Python or C code | Buffer overflow in an image decoder, integer overflow in `ImagingNew` | +| Vulnerability in a bundled or wheel-shipped C library | libjpeg, libwebp, libtiff, libpng, openjpeg, libavif | +| Supply-chain compromise | Malicious commit, stolen maintainer credentials, tampered PyPI wheel | +| CI/CD or infrastructure compromise | GitHub Actions secret leak, Codecov breach, PyPI token exposure | +| Critical non-security regression | Data-loss bug shipped in a release, crash on all supported platforms | + +--- + +## 3. Roles + +| Role | Responsibility | +|---|---| +| **Incident Lead** | First maintainer to triage the report. Owns the incident until resolution. | +| **Patch Owner** | Writes and tests the fix (may be the same person as Incident Lead). | +| **Release Manager** | Cuts the point release following [RELEASING.md](../RELEASING.md). | +| **Communications Owner** | Drafts the GitHub Security Advisory, announces on Mastodon, notifies distros. | +| **Tidelift Contact** | For reports that arrive via Tidelift, coordinate through the Tidelift security portal. | + +For the typical small maintainer team, one person may fill multiple roles. Assign roles +explicitly at the start of each incident to avoid gaps. + +--- + +## 4. Severity Classification + +Use the [CVSS v3.1](https://www.first.org/cvss/v3.1/specification-document) base score as +a guide, mapped to the following levels: + +| Severity | CVSS | Definition | Target Response SLA | +|---|---|---|---| +| **Critical** | 9.0 – 10.0 | Remote code execution, arbitrary write, or complete integrity/confidentiality loss achievable by opening a crafted image | 48 hours to patch; embargoed release where possible | +| **High** | 7.0 – 8.9 | Heap/stack buffer overflow, use-after-free, or significant information disclosure | 7 days to patch | +| **Medium** | 4.0 – 6.9 | Denial of service via crafted image, out-of-bounds read, limited info disclosure | Next scheduled quarterly release, or earlier point release if needed | +| **Low** | 0.1 – 3.9 | Minor information disclosure, unlikely to be exploitable in practice | Next quarterly release | + +Supply-chain and CI/CD incidents are always treated as **Critical** regardless of CVSS. + +--- + +## 5. Detection Sources + +Vulnerabilities and incidents may be reported or discovered through: + +1. **GitHub private security advisory** — preferred channel; see [SECURITY.md](SECURITY.md) +2. **Tidelift security contact** — +3. **Direct maintainer contact** — DM on Mastodon or email +4. **External researcher / coordinated disclosure** — e.g. Google Project Zero, vendor PSIRT +5. **Automated scanning** — Dependabot, GitHub code-scanning (CodeQL), CI fuzzing +6. **Distro security teams** — Debian, Red Hat, Ubuntu, Alpine may report upstream +7. **User bug report** — public issue (reassess if it has security implications before it stays public) + +--- + +## 6. Response Process + +### 6.1 Triage (all severities) + +1. **Acknowledge receipt** to the reporter within **72 hours** using the template in + [Appendix A](#appendix-a-communication-templates). Ask the reporter: + - How they would like to be credited (name, handle, or anonymous) + - Whether they intend to publish their own advisory, and if so, their preferred timeline + - Thank them explicitly — reporters do the project a favour by disclosing privately. +2. Reproduce the issue. If the report is invalid, close it and notify the reporter. +3. Assign a severity level (Section 3) and an Incident Lead. +4. If the GitHub Security Advisory was not created by the reporter, create one now and keep + it **private** until the fix is released. Add the reporter as a collaborator if they wish + to be involved. +5. **Request a CVE** through the GitHub Security Advisory workflow (GitHub is a CVE + Numbering Authority — no separate MITRE form required). The CVE is reserved privately + and published automatically when the advisory goes public. +6. Notify Tidelift if the severity is High or Critical. +7. **Escalation** — Escalate beyond the core maintainer team if any of the following apply: + - The vulnerability is being actively exploited in the wild → notify [GitHub Security](mailto:security@github.com) and the [Python Security Response Team](https://www.python.org/news/security/) + - The fix requires changes to CPython or a dependency outside Pillow's control → contact the relevant upstream immediately + - A legal concern arises (e.g. GDPR-reportable data exposure) → contact the project's legal/fiscal sponsor + - The Incident Lead is unreachable for > 24 hours on a Critical issue → any other maintainer may assume the role + +### 6.2 Fix Development + +1. Develop the fix in a **private fork** or directly in the private security advisory + workspace on GitHub. Do **not** push to a public branch before the embargo lifts. +2. Write a regression test that fails before the fix and passes after. +3. Run the full test suite locally across all supported Python versions: + ```bash + make release-test + ``` +4. Review the patch with at least one other maintainer. + +### 6.3 Standard (Non-Embargoed) Release + +For Medium and Low severity, or when no distro pre-notification is needed: + +1. Merge the fix to `main`, then cherry-pick to all affected release branches + (see [RELEASING.md — Point release](../RELEASING.md)). +2. Amend commit messages to include the CVE identifier. +3. Tag and push; the GitHub Actions "Wheels" workflow will build and upload to PyPI. +4. Publish the GitHub Security Advisory (this simultaneously publishes the CVE). +5. Announce on [Mastodon](https://fosstodon.org/@pillow). + +### 6.4 Embargoed Release + +For Critical and High severity where distro pre-notification improves user safety: + +1. Prepare patches against all affected release branches and test locally. +2. Agree on an **embargo date** with the reporter (typically 7–14 days out, up to 90 days for + complex issues). +3. Privately send the patch to distros via the + [linux-distros](https://oss-security.openwall.org/wiki/mailing-lists/distros) mailing list + or directly to individual distro security teams. +4. On the embargo date: + - Amend commit messages with the CVE identifier. + - Tag and push all affected release branches (see [RELEASING.md — Embargoed release](../RELEASING.md)). + - Confirm the "Wheels" workflow has passed and wheels are live on PyPI. + - Publish the GitHub Security Advisory. + - Announce on [Mastodon](https://fosstodon.org/@pillow). + +### 6.5 Rollback Procedures + +If a security patch introduces a critical regression after release: + +1. **Yank the release immediately** via the PyPI web interface: + [https://pypi.org/manage/project/pillow/release/\/](https://pypi.org/manage/project/pillow/) + (navigate to the release, click **"Yank"**). + Yanked releases remain downloadable by pinned users but are excluded from `pip install` + resolution, giving time to fix without leaving users unpatched. +2. Post a public notice in the GitHub release and on Mastodon explaining the regression and + that the release has been yanked. +3. If the previous (vulnerable) version was also yanked, **un-yank it temporarily** so users + have a functional fallback while the corrected release is prepared. +4. Prepare a corrected point release (incrementing the patch version), repeating §6.2–§6.3. +5. Document the regression in the post-incident review (§9). + +### 6.6 Supply-Chain / Infrastructure Compromise + +1. **Immediately** revoke any potentially compromised credentials: + - PyPI API tokens (regenerate and update in GitHub secrets) + - GitHub personal access tokens and OAuth apps + - Codecov or other CI service tokens +2. Audit recent commits and releases for tampering: + - Verify release tags against known-good SHAs + - Re-inspect any wheel published since the potential compromise window +3. If a PyPI release is suspected to be tampered: yank it immediately via + [https://pypi.org/manage/project/pillow/](https://pypi.org/manage/project/pillow/); + file a report with the [PyPI security team](mailto:security@pypi.org). +4. Notify GitHub Security if repository access or Actions secrets are involved. +5. Issue a public advisory describing the scope and any user action required. + +--- + +## 7. Communication + +### Internal (during embargo) +- Use the **private GitHub Security Advisory** thread for all coordination. +- Do not discuss details in public issues, PRs, or Gitter/IRC channels. + +### External (at or after disclosure) + +| Audience | Channel | Timing | +|---|---|---| +| General users | [GitHub Security Advisory](https://github.com/python-pillow/Pillow/security/advisories) | At release | +| PyPI ecosystem | CVE published via advisory | At release | +| Downstream distros | Direct email or linux-distros list | Before embargo date (embargoed) | +| Tidelift subscribers | Tidelift security portal | At release (or coordinated) | +| Community | [Mastodon @pillow](https://fosstodon.org/@pillow) | At release | + +**Advisory content should include:** +- CVE identifier and CVSS score +- Affected Pillow versions +- Fixed version(s) +- Nature of the vulnerability (without full exploit details if still fresh) +- Credit to the reporter (with their consent) +- Upgrade instructions (`pip install --upgrade Pillow`) + +--- + +## 8. Post-Incident Review + +Within **2 weeks** of a Critical or High severity fix being released: + +1. Hold a brief retrospective (async is fine for a distributed team). +2. Document the following metrics for the incident record: + + | Metric | Target | Actual | + |---|---|---| + | Time to acknowledge reporter | ≤ 72 hours | | + | Time to reproduce & assess severity | ≤ 5 days | | + | Time to develop & review fix | Varies by severity | | + | Time from report to public release | Critical ≤ 14 days; High ≤ 30 days | | + +3. Record: + - What went well + - What could be improved + - Root cause: what allowed the vulnerability to exist + - Whether any distro/downstream was impacted before the fix was available +4. File follow-up issues for any process improvements identified. +5. Update this document if the response process needs revision. + +--- + +## 9. Dependency Map + +Understanding what Pillow depends on (upstream) and what depends on Pillow (downstream) +is essential for scoping impact and coordinating notifications during an incident. + +### 9.1 Upstream Dependencies + +#### Bundled C libraries (shipped in official wheels) + +These libraries are compiled into Pillow's binary wheels. A CVE in any of them may +require a Pillow point release even if Pillow's own code is unchanged. + +| Library | Purpose | Security advisory tracker | +|---|---|---| +| [libjpeg-turbo](https://libjpeg-turbo.org/) | JPEG encode/decode | [GitHub](https://github.com/libjpeg-turbo/libjpeg-turbo/security) | +| [libpng](http://www.libpng.org/pub/png/libpng.html) | PNG encode/decode | [SourceForge](https://sourceforge.net/p/libpng/bugs/) | +| [libtiff](https://libtiff.gitlab.io/libtiff/) | TIFF encode/decode | [GitLab](https://gitlab.com/libtiff/libtiff/-/issues) | +| [libwebp](https://chromium.googlesource.com/webm/libwebp) | WebP encode/decode | [Chromium tracker](https://bugs.chromium.org/p/webm/) | +| [libavif](https://github.com/AOMediaCodec/libavif) | AVIF encode/decode | [GitHub](https://github.com/AOMediaCodec/libavif/security) | +| [aom](https://aomedia.googlesource.com/aom/) | AV1 codec (AVIF) | [Chromium tracker](https://bugs.chromium.org/p/aomedia/) | +| [dav1d](https://code.videolan.org/videolan/dav1d) | AV1 decode (AVIF) | [VideoLAN](https://security.videolan.org/) | +| [openjpeg](https://www.openjpeg.org/) | JPEG 2000 encode/decode | [GitHub](https://github.com/uclouvain/openjpeg/security) | +| [freetype2](https://freetype.org/) | Font rendering | [GitLab](https://gitlab.freedesktop.org/freetype/freetype/-/issues) | +| [lcms2](https://www.littlecms.com/) | ICC color management | [GitHub](https://github.com/mm2/Little-CMS) | +| [harfbuzz](https://harfbuzz.github.io/) | Text shaping (via raqm) | [GitHub](https://github.com/harfbuzz/harfbuzz/security) | +| [raqm](https://github.com/HOST-Oman/libraqm) | Complex text layout | [GitHub](https://github.com/HOST-Oman/libraqm) | +| [fribidi](https://github.com/fribidi/fribidi) | Unicode bidi (via raqm) | [GitHub](https://github.com/fribidi/fribidi) | +| [zlib](https://zlib.net/) | Deflate compression | [zlib.net](https://zlib.net/) | +| [liblzma / xz-utils](https://tukaani.org/xz/) | XZ/LZMA compression | [GitHub](https://github.com/tukaani-project/xz) | +| [bzip2](https://sourceware.org/bzip2/) | BZ2 compression | [Sourceware](https://sourceware.org/bzip2/) | +| [zstd](https://github.com/facebook/zstd) | Zstandard compression | [GitHub](https://github.com/facebook/zstd/security) | +| [brotli](https://github.com/google/brotli) | Brotli compression | [GitHub](https://github.com/google/brotli) | +| [libyuv](https://chromium.googlesource.com/libyuv/libyuv/) | YUV conversion | [Chromium tracker](https://bugs.chromium.org/p/libyuv/) | + +#### Python-level dependencies + +| Package | Required? | Purpose | +|---|---|---| +| `pybind11` | Build-time only | C++ ↔ Python bindings | +| `olefile` | Optional (`fpx`, `mic` extras) | OLE2 container parsing (FPX, MIC formats) | +| `defusedxml` | Optional (`xmp` extra) | Safe XML parsing for XMP metadata | + +### 9.2 Downstream Dependencies + +A vulnerability in Pillow can have wide impact. Notify or consider the blast radius of +these downstream consumers when assessing severity and planning communications. + +#### Linux distribution packages + +| Distribution | Package name | Security contact | +|---|---|---| +| Debian / Ubuntu | `python3-pil` | [Debian Security](https://www.debian.org/security/) / [Ubuntu Security](https://ubuntu.com/security) | +| Fedora / RHEL / CentOS | `python3-pillow` | [Red Hat Security](https://access.redhat.com/security/) | +| Alpine Linux | `py3-pillow` | [Alpine security](https://security.alpinelinux.org/) | +| Arch Linux | `python-pillow` | [Arch security tracker](https://security.archlinux.org/) | +| Homebrew (macOS) | `pillow` | [Homebrew maintainers](https://github.com/Homebrew/homebrew-core) | +| conda-forge | `pillow` | [conda-forge](https://github.com/conda-forge/pillow-feedstock) | + +#### Major Python ecosystem consumers + +These are high-profile projects known to depend on Pillow; a critical vulnerability may +warrant proactive notification. + +| Project | Usage | +|---|---| +| [matplotlib](https://matplotlib.org/) | Image I/O for plots | +| [scikit-image](https://scikit-image.org/) | Image processing | +| [torchvision](https://github.com/pytorch/vision) (PyTorch) | Dataset loading, transforms | +| [Keras / TensorFlow](https://keras.io/) | Image preprocessing utilities | +| [Django](https://www.djangoproject.com/) | `ImageField` validation and thumbnail generation | +| [Wagtail](https://wagtail.org/) | CMS image renditions | +| [Plone](https://plone.org/) | CMS image handling | +| [Jupyter / IPython](https://jupyter.org/) | Inline image display | +| [ReportLab](https://www.reportlab.com/) | PDF image embedding | +| [Wand](https://docs.wand-py.org/) | Sometimes used alongside Pillow | +| [Tidelift subscribers](https://tidelift.com/) | Enterprise consumers (coordinated via Tidelift) | + +#### Pillow ecosystem plugins + +Third-party plugins extend Pillow and are distributed separately on PyPI. Their +maintainers should be notified for Critical/High issues that affect the plugin API +or the formats they decode. See the +[full plugin list](https://pillow.readthedocs.io/en/stable/handbook/third-party-plugins.html). + +### 9.3 Responding to an Upstream Vulnerability + +When a CVE is published for a bundled C library: + +1. Assess whether the vulnerable code path is reachable through Pillow's API. +2. If reachable, treat as a Pillow vulnerability and follow Section 5. +3. Update the bundled library version in the wheel build scripts and rebuild wheels. +4. Reference the upstream CVE in Pillow's release notes and GitHub Security Advisory. +5. If not reachable, document the rationale in a public issue so downstream distributors + can make informed decisions about patching their system packages. + +--- + +## 10. References + +- [Security Policy](SECURITY.md) +- [Release Checklist](../RELEASING.md) +- [Contributing Guide](CONTRIBUTING.md) +- [Tidelift Security Contact](https://tidelift.com/security) +- [GitHub: Privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability) +- [GitHub as a CVE Numbering Authority (CNA)](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/about-github-security-advisories-for-repositories#cve-identification-numbers) +- [FIRST CVSS v3.1 Calculator](https://www.first.org/cvss/calculator/3.1) +- [linux-distros mailing list](https://oss-security.openwall.org/wiki/mailing-lists/distros) +- [OpenSSF CVD Guide](https://github.com/ossf/oss-vulnerability-guide) *(basis for this plan)* + +--- + +## Appendix A: Communication Templates + +### A.1 Reporter Acknowledgment + +> Subject: Re: [Security] \ +> +> Hi \, +> +> Thank you for taking the time to report this — we genuinely appreciate it. +> +> We have received your report and will assess it within the next few days. We will keep +> you updated on our progress. +> +> A few quick questions so we can handle this well: +> - How would you like to be credited in the advisory? (name, handle, organisation, or anonymous) +> - Do you plan to publish your own write-up or advisory? If so, is there a disclosure date +> that works for you? +> +> We aim to treat all vulnerability reports in line with coordinated disclosure principles. +> If you have any questions or concerns at any point, please reply to this thread. +> +> Thanks again, +> The Pillow maintainers + +### A.2 Embargoed Distro Notification + +> Subject: [EMBARGOED] Pillow security issue — \ — disclosure \ +> +> This is an embargoed notification of a vulnerability in Pillow. Please keep this +> information confidential until the disclosure date listed below. +> +> **CVE:** \ +> **Affected versions:** \ +> **Fixed version:** \ +> **Severity:** \ (CVSS \: \) +> **Reporter:** \ +> **Public disclosure date:** \ +> +> **Summary:** +> \ +> +> **Proof of concept:** +> \ +> +> **Remediation:** +> Upgrade to Pillow \. No known workaround. +> +> Please do not share this information, issue public patches, or make user communications +> before the disclosure date. We will notify this list immediately if the date changes. +> +> — The Pillow maintainers + +### A.3 Public Disclosure Advisory + +*(Published as a GitHub Security Advisory; the CVE and date are included automatically.)* + +> **Summary:** \ +> +> **CVE:** \ +> **Affected versions:** Pillow \< \ +> **Fixed version:** \ +> **Severity:** \ (CVSS \) +> **Reporter:** \ +> +> **Details:** +> \ +> +> **Remediation:** +> ``` +> pip install --upgrade Pillow +> ``` +> +> **Timeline:** +> - Reported: \ +> - Fixed: \ +> - Disclosed: \ From 4d63d0b3a6c5deccce7f33ecf1e6c3d74ee9d8a5 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 12:47:50 -0400 Subject: [PATCH 07/83] Fix links --- .github/INCIDENT_RESPONSE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 03371c4f3..65b1f1b7d 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -174,7 +174,7 @@ For Critical and High severity where distro pre-notification improves user safet If a security patch introduces a critical regression after release: 1. **Yank the release immediately** via the PyPI web interface: - [https://pypi.org/manage/project/pillow/release/\/](https://pypi.org/manage/project/pillow/) + [https://pypi.org/manage/project/pillow/](https://pypi.org/manage/project/pillow/) (navigate to the release, click **"Yank"**). Yanked releases remain downloadable by pinned users but are excluded from `pip install` resolution, giving time to fix without leaving users unpatched. @@ -272,7 +272,7 @@ require a Pillow point release even if Pillow's own code is unchanged. | [libwebp](https://chromium.googlesource.com/webm/libwebp) | WebP encode/decode | [Chromium tracker](https://bugs.chromium.org/p/webm/) | | [libavif](https://github.com/AOMediaCodec/libavif) | AVIF encode/decode | [GitHub](https://github.com/AOMediaCodec/libavif/security) | | [aom](https://aomedia.googlesource.com/aom/) | AV1 codec (AVIF) | [Chromium tracker](https://bugs.chromium.org/p/aomedia/) | -| [dav1d](https://code.videolan.org/videolan/dav1d) | AV1 decode (AVIF) | [VideoLAN](https://security.videolan.org/) | +| [dav1d](https://code.videolan.org/videolan/dav1d) | AV1 decode (AVIF) | [VideoLAN Security](https://www.videolan.org/security/) | | [openjpeg](https://www.openjpeg.org/) | JPEG 2000 encode/decode | [GitHub](https://github.com/uclouvain/openjpeg/security) | | [freetype2](https://freetype.org/) | Font rendering | [GitLab](https://gitlab.freedesktop.org/freetype/freetype/-/issues) | | [lcms2](https://www.littlecms.com/) | ICC color management | [GitHub](https://github.com/mm2/Little-CMS) | @@ -281,7 +281,7 @@ require a Pillow point release even if Pillow's own code is unchanged. | [fribidi](https://github.com/fribidi/fribidi) | Unicode bidi (via raqm) | [GitHub](https://github.com/fribidi/fribidi) | | [zlib](https://zlib.net/) | Deflate compression | [zlib.net](https://zlib.net/) | | [liblzma / xz-utils](https://tukaani.org/xz/) | XZ/LZMA compression | [GitHub](https://github.com/tukaani-project/xz) | -| [bzip2](https://sourceware.org/bzip2/) | BZ2 compression | [Sourceware](https://sourceware.org/bzip2/) | +| [bzip2](https://gitlab.com/bzip2/bzip2) | BZ2 compression | [GitLab](https://gitlab.com/bzip2/bzip2/-/issues) | | [zstd](https://github.com/facebook/zstd) | Zstandard compression | [GitHub](https://github.com/facebook/zstd/security) | | [brotli](https://github.com/google/brotli) | Brotli compression | [GitHub](https://github.com/google/brotli) | | [libyuv](https://chromium.googlesource.com/libyuv/libyuv/) | YUV conversion | [Chromium tracker](https://bugs.chromium.org/p/libyuv/) | @@ -356,7 +356,7 @@ When a CVE is published for a bundled C library: - [Contributing Guide](CONTRIBUTING.md) - [Tidelift Security Contact](https://tidelift.com/security) - [GitHub: Privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability) -- [GitHub as a CVE Numbering Authority (CNA)](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/about-github-security-advisories-for-repositories#cve-identification-numbers) +- [GitHub as a CVE Numbering Authority (CNA)](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/about-repository-security-advisories) - [FIRST CVSS v3.1 Calculator](https://www.first.org/cvss/calculator/3.1) - [linux-distros mailing list](https://oss-security.openwall.org/wiki/mailing-lists/distros) - [OpenSSF CVD Guide](https://github.com/ossf/oss-vulnerability-guide) *(basis for this plan)* From cdaa1bf9ef0faca5ddf2f2e7d8742678552a90d1 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 12:57:16 -0400 Subject: [PATCH 08/83] Add sections from Bootstrap example At the risk of making this document larger, add in sections in Bootstrap IRP but not ours. - https://github.com/twbs/bootstrap/blob/main/.github/INCIDENT_RESPONSE.md --- .github/INCIDENT_RESPONSE.md | 75 +++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 65b1f1b7d..6fdbfff2e 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -61,7 +61,21 @@ This plan covers: --- -## 3. Roles +## 3. Definitions + +| Term | Meaning | +|---|---| +| **Incident** | Any event that compromises or threatens the confidentiality, integrity, or availability of Pillow's code, release artifacts, or infrastructure. | +| **Vulnerability** | A security flaw in Pillow or a bundled library that can be exploited by a crafted image or API call. | +| **Incident Lead** | The maintainer who owns coordination of the response from triage to closure. | +| **Embargo** | A period during which fix details are kept private to allow coordinated patching before public disclosure. | +| **Yank** | A PyPI action that keeps a release downloadable by pinned users but removes it from default `pip install` resolution. | +| **CVE** | Common Vulnerabilities and Exposures — a public identifier assigned to a specific vulnerability. | +| **CNA** | CVE Numbering Authority — GitHub is a CNA and can assign CVEs directly through the advisory workflow. | + +--- + +## 4. Roles | Role | Responsibility | |---|---| @@ -76,7 +90,7 @@ explicitly at the start of each incident to avoid gaps. --- -## 4. Severity Classification +## 5. Severity Classification Use the [CVSS v3.1](https://www.first.org/cvss/v3.1/specification-document) base score as a guide, mapped to the following levels: @@ -90,9 +104,11 @@ a guide, mapped to the following levels: Supply-chain and CI/CD incidents are always treated as **Critical** regardless of CVSS. +> **Note:** These are good-faith targets for a small volunteer maintainer team, not contractual SLAs. Public safety and transparency will always be prioritised, even when timing varies. + --- -## 5. Detection Sources +## 6. Detection Sources Vulnerabilities and incidents may be reported or discovered through: @@ -106,9 +122,9 @@ Vulnerabilities and incidents may be reported or discovered through: --- -## 6. Response Process +## 7. Response Process -### 6.1 Triage (all severities) +### 7.1 Triage (all severities) 1. **Acknowledge receipt** to the reporter within **72 hours** using the template in [Appendix A](#appendix-a-communication-templates). Ask the reporter: @@ -130,7 +146,7 @@ Vulnerabilities and incidents may be reported or discovered through: - A legal concern arises (e.g. GDPR-reportable data exposure) → contact the project's legal/fiscal sponsor - The Incident Lead is unreachable for > 24 hours on a Critical issue → any other maintainer may assume the role -### 6.2 Fix Development +### 7.2 Fix Development 1. Develop the fix in a **private fork** or directly in the private security advisory workspace on GitHub. Do **not** push to a public branch before the embargo lifts. @@ -141,7 +157,7 @@ Vulnerabilities and incidents may be reported or discovered through: ``` 4. Review the patch with at least one other maintainer. -### 6.3 Standard (Non-Embargoed) Release +### 7.3 Standard (Non-Embargoed) Release For Medium and Low severity, or when no distro pre-notification is needed: @@ -152,7 +168,7 @@ For Medium and Low severity, or when no distro pre-notification is needed: 4. Publish the GitHub Security Advisory (this simultaneously publishes the CVE). 5. Announce on [Mastodon](https://fosstodon.org/@pillow). -### 6.4 Embargoed Release +### 7.4 Embargoed Release For Critical and High severity where distro pre-notification improves user safety: @@ -169,7 +185,7 @@ For Critical and High severity where distro pre-notification improves user safet - Publish the GitHub Security Advisory. - Announce on [Mastodon](https://fosstodon.org/@pillow). -### 6.5 Rollback Procedures +### 7.5 Rollback Procedures If a security patch introduces a critical regression after release: @@ -182,10 +198,10 @@ If a security patch introduces a critical regression after release: that the release has been yanked. 3. If the previous (vulnerable) version was also yanked, **un-yank it temporarily** so users have a functional fallback while the corrected release is prepared. -4. Prepare a corrected point release (incrementing the patch version), repeating §6.2–§6.3. +4. Prepare a corrected point release (incrementing the patch version), repeating §7.2–§7.3. 5. Document the regression in the post-incident review (§9). -### 6.6 Supply-Chain / Infrastructure Compromise +### 7.6 Supply-Chain / Infrastructure Compromise 1. **Immediately** revoke any potentially compromised credentials: - PyPI API tokens (regenerate and update in GitHub secrets) @@ -200,9 +216,19 @@ If a security patch introduces a critical regression after release: 4. Notify GitHub Security if repository access or Actions secrets are involved. 5. Issue a public advisory describing the scope and any user action required. +### 7.7 Recovery + +After the fix is released and the advisory is public: + +1. Verify that the patched wheels are live on PyPI and passing CI across all supported platforms. +2. Confirm any yanked releases are handled correctly (re-yank if un-yanked as a fallback during rollback). +3. Resume normal development operations on `main`. +4. Monitor the GitHub issue tracker and Mastodon for user reports of residual problems for at least **72 hours** post-release. +5. Close the private GitHub Security Advisory once recovery is confirmed. + --- -## 7. Communication +## 8. Communication ### Internal (during embargo) - Use the **private GitHub Security Advisory** thread for all coordination. @@ -228,7 +254,7 @@ If a security patch introduces a critical regression after release: --- -## 8. Post-Incident Review +## 9. Post-Incident Review Within **2 weeks** of a Critical or High severity fix being released: @@ -252,12 +278,12 @@ Within **2 weeks** of a Critical or High severity fix being released: --- -## 9. Dependency Map +## 10. Dependency Map Understanding what Pillow depends on (upstream) and what depends on Pillow (downstream) is essential for scoping impact and coordinating notifications during an incident. -### 9.1 Upstream Dependencies +### 10.1 Upstream Dependencies #### Bundled C libraries (shipped in official wheels) @@ -294,7 +320,7 @@ require a Pillow point release even if Pillow's own code is unchanged. | `olefile` | Optional (`fpx`, `mic` extras) | OLE2 container parsing (FPX, MIC formats) | | `defusedxml` | Optional (`xmp` extra) | Safe XML parsing for XMP metadata | -### 9.2 Downstream Dependencies +### 10.2 Downstream Dependencies A vulnerability in Pillow can have wide impact. Notify or consider the blast radius of these downstream consumers when assessing severity and planning communications. @@ -336,7 +362,7 @@ maintainers should be notified for Critical/High issues that affect the plugin A or the formats they decode. See the [full plugin list](https://pillow.readthedocs.io/en/stable/handbook/third-party-plugins.html). -### 9.3 Responding to an Upstream Vulnerability +### 10.3 Responding to an Upstream Vulnerability When a CVE is published for a bundled C library: @@ -349,7 +375,20 @@ When a CVE is published for a bundled C library: --- -## 10. References +## 11. Plan Maintenance + +This document is a living record. It should be kept current so it is useful when an +incident actually occurs. + +- **Annual review** — revisit during the §1.3 readiness review each January. +- **Post-incident update** — if the response process revealed gaps or needed improvisation, + update this document before the post-incident review is closed (§9). +- **Ownership** — changes are approved by the Core Team and recorded in Git history. + Substantive changes should be noted in the PR description so they are easy to find later. + +--- + +## 12. References - [Security Policy](SECURITY.md) - [Release Checklist](../RELEASING.md) From 64ed4710b9282d4547ee62883e3f496f2de31fb0 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 16:59:41 -0400 Subject: [PATCH 09/83] Fix version support matrix to reflect main-only security policy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 6fdbfff2e..921dfedd8 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -12,17 +12,18 @@ Maintaining readiness before an incident occurs reduces response time and errors ### 1.1 Version Support Matrix -Only the following branches receive security fixes. Reporters should verify their affected -version before filing; maintainers should cherry-pick fixes only to supported branches. +Security fixes are applied to the **latest stable release only**. Users on older versions +are expected to upgrade. This is consistent with Pillow's quarterly release cadence and +is not currently documented elsewhere — reporters should assume only the latest release +will receive a patch. -| Branch | Status | Notes | -|---|---|---| -| `main` | ✅ Active development | Always patched | -| Latest stable (e.g. `11.x`) | ✅ Security fixes | Current quarterly release series | -| Previous stable (e.g. `10.x`) | ⚠️ Critical only | One release series back; Critical CVEs only | -| Older branches | ❌ End of life | No security support; users must upgrade | +| Branch | Status | +|---|---| +| `main` / latest stable | ✅ Security fixes applied | +| All older releases | ❌ No security support — please upgrade | -> Update this table with each quarterly release. +> If backport support for older releases is ever added, update this table and document it +> in [SECURITY.md](SECURITY.md). ### 1.2 Team Readiness From 4a74a20b86b9a6c4836b54f336356accfdf53278 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 17:02:24 -0400 Subject: [PATCH 10/83] Update Readiness Review: quarterly cadence, trim checklist Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 921dfedd8..8e35754aa 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -27,24 +27,15 @@ will receive a patch. ### 1.2 Team Readiness -- Maintain a private list of current maintainer contact details (GitHub handles, email, - Mastodon) in a location accessible to all maintainers (e.g. a pinned private team - discussion or the Tidelift maintainer portal). -- Ensure at least two maintainers have admin access to: - - The GitHub repository (to manage Security Advisories) - - The [PyPI Pillow project](https://pypi.org/project/Pillow/) (to yank releases) - - The Tidelift maintainer portal -- Rotate and audit PyPI API tokens and GitHub Actions secrets at least once per year, - and immediately after any maintainer leaves the project. +The four members of the Pillow core team are in regular contact and share collective +responsibility for incident response. Any core team member may act as Incident Lead. +Contact details are known to all team members. -### 1.3 Annual Readiness Review +### 1.3 Readiness Review -Once per year (suggested: at the January quarterly release), maintainers should: +At each quarterly release, maintainers should: 1. Re-read this document and update any stale content (version table, contacts, tooling). -2. Verify the GitHub private security advisory flow still works (open and close a test advisory). -3. Confirm PyPI yank access is functional. -4. Review Dependabot and CodeQL alert settings are enabled on the repository. --- @@ -381,7 +372,7 @@ When a CVE is published for a bundled C library: This document is a living record. It should be kept current so it is useful when an incident actually occurs. -- **Annual review** — revisit during the §1.3 readiness review each January. +- **Quarterly review** — revisit during the §1.3 readiness review at each quarterly release. - **Post-incident update** — if the response process revealed gaps or needed improvisation, update this document before the post-incident review is closed (§9). - **Ownership** — changes are approved by the Core Team and recorded in Git history. From 3aa076129fa66eefa213c9708779ce36c832ed93 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 17:25:32 -0400 Subject: [PATCH 11/83] Remove backport comment from version support matrix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 8e35754aa..0662a3a31 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -22,9 +22,6 @@ will receive a patch. | `main` / latest stable | ✅ Security fixes applied | | All older releases | ❌ No security support — please upgrade | -> If backport support for older releases is ever added, update this table and document it -> in [SECURITY.md](SECURITY.md). - ### 1.2 Team Readiness The four members of the Pillow core team are in regular contact and share collective From c2ac2da31ccabc8d9c49f7bad1d36e9578b97e44 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 17:28:42 -0400 Subject: [PATCH 12/83] Inline Readiness Review procedure as prose Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 0662a3a31..186d054c6 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -30,9 +30,7 @@ Contact details are known to all team members. ### 1.3 Readiness Review -At each quarterly release, maintainers should: - -1. Re-read this document and update any stale content (version table, contacts, tooling). +At each quarterly release, maintainers should re-read this document and update any stale content. --- From ad582c1a8eac0c609ca84fea80d938a58dfe0597 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 17:38:34 -0400 Subject: [PATCH 13/83] Simplify Roles section note Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 186d054c6..c6898969a 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -72,8 +72,7 @@ This plan covers: | **Communications Owner** | Drafts the GitHub Security Advisory, announces on Mastodon, notifies distros. | | **Tidelift Contact** | For reports that arrive via Tidelift, coordinate through the Tidelift security portal. | -For the typical small maintainer team, one person may fill multiple roles. Assign roles -explicitly at the start of each incident to avoid gaps. +One person may fill multiple roles. --- From e0f9e2b98ef9d4cf278117160f4aa86f7b4e44ba Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 17:46:58 -0400 Subject: [PATCH 14/83] Fix severity classification cross-reference, remove incident lead assignment step Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index c6898969a..9cb6ba9db 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -118,7 +118,7 @@ Vulnerabilities and incidents may be reported or discovered through: - Whether they intend to publish their own advisory, and if so, their preferred timeline - Thank them explicitly — reporters do the project a favour by disclosing privately. 2. Reproduce the issue. If the report is invalid, close it and notify the reporter. -3. Assign a severity level (Section 3) and an Incident Lead. +3. Assign a severity level ([§5 Severity Classification](#5-severity-classification)). 4. If the GitHub Security Advisory was not created by the reporter, create one now and keep it **private** until the fix is released. Add the reporter as a collaborator if they wish to be involved. From 68be7f30ff2872a474c4de9484f44061733eaf78 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 17:50:45 -0400 Subject: [PATCH 15/83] Remove Tidelift notification step from triage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 9cb6ba9db..af4217876 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -125,8 +125,7 @@ Vulnerabilities and incidents may be reported or discovered through: 5. **Request a CVE** through the GitHub Security Advisory workflow (GitHub is a CVE Numbering Authority — no separate MITRE form required). The CVE is reserved privately and published automatically when the advisory goes public. -6. Notify Tidelift if the severity is High or Critical. -7. **Escalation** — Escalate beyond the core maintainer team if any of the following apply: +6. **Escalation** — Escalate beyond the core maintainer team if any of the following apply: - The vulnerability is being actively exploited in the wild → notify [GitHub Security](mailto:security@github.com) and the [Python Security Response Team](https://www.python.org/news/security/) - The fix requires changes to CPython or a dependency outside Pillow's control → contact the relevant upstream immediately - A legal concern arises (e.g. GDPR-reportable data exposure) → contact the project's legal/fiscal sponsor From 3f90d5c4da6efc69641ecee639b1eba6dbb20bc7 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 17:53:04 -0400 Subject: [PATCH 16/83] =?UTF-8?q?Replace=20section=20sign=20(=C2=A7)=20wit?= =?UTF-8?q?h=20plain=20Section=20references?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index af4217876..5295a48db 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -118,7 +118,7 @@ Vulnerabilities and incidents may be reported or discovered through: - Whether they intend to publish their own advisory, and if so, their preferred timeline - Thank them explicitly — reporters do the project a favour by disclosing privately. 2. Reproduce the issue. If the report is invalid, close it and notify the reporter. -3. Assign a severity level ([§5 Severity Classification](#5-severity-classification)). +3. Assign a severity level ([Section 5: Severity Classification](#5-severity-classification)). 4. If the GitHub Security Advisory was not created by the reporter, create one now and keep it **private** until the fix is released. Add the reporter as a collaborator if they wish to be involved. @@ -183,8 +183,8 @@ If a security patch introduces a critical regression after release: that the release has been yanked. 3. If the previous (vulnerable) version was also yanked, **un-yank it temporarily** so users have a functional fallback while the corrected release is prepared. -4. Prepare a corrected point release (incrementing the patch version), repeating §7.2–§7.3. -5. Document the regression in the post-incident review (§9). +4. Prepare a corrected point release (incrementing the patch version), repeating sections 7.2–7.3. +5. Document the regression in the post-incident review (Section 9). ### 7.6 Supply-Chain / Infrastructure Compromise @@ -365,9 +365,9 @@ When a CVE is published for a bundled C library: This document is a living record. It should be kept current so it is useful when an incident actually occurs. -- **Quarterly review** — revisit during the §1.3 readiness review at each quarterly release. +- **Quarterly review** — revisit during the Section 1.3 readiness review at each quarterly release. - **Post-incident update** — if the response process revealed gaps or needed improvisation, - update this document before the post-incident review is closed (§9). + update this document before the post-incident review is closed (Section 9). - **Ownership** — changes are approved by the Core Team and recorded in Git history. Substantive changes should be noted in the PR description so they are easy to find later. From 20af4ec89c6dd568f256864c4f5407595238e3b3 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 17:55:11 -0400 Subject: [PATCH 17/83] Change Critical/High SLA targets to best effort Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 5295a48db..f8cabe88d 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -83,8 +83,8 @@ a guide, mapped to the following levels: | Severity | CVSS | Definition | Target Response SLA | |---|---|---|---| -| **Critical** | 9.0 – 10.0 | Remote code execution, arbitrary write, or complete integrity/confidentiality loss achievable by opening a crafted image | 48 hours to patch; embargoed release where possible | -| **High** | 7.0 – 8.9 | Heap/stack buffer overflow, use-after-free, or significant information disclosure | 7 days to patch | +| **Critical** | 9.0 – 10.0 | Remote code execution, arbitrary write, or complete integrity/confidentiality loss achievable by opening a crafted image | Best effort; embargoed release where possible | +| **High** | 7.0 – 8.9 | Heap/stack buffer overflow, use-after-free, or significant information disclosure | Best effort | | **Medium** | 4.0 – 6.9 | Denial of service via crafted image, out-of-bounds read, limited info disclosure | Next scheduled quarterly release, or earlier point release if needed | | **Low** | 0.1 – 3.9 | Minor information disclosure, unlikely to be exploitable in practice | Next quarterly release | From e74a89f70e419a4035428bc558e6a674ba53dab6 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 17:59:29 -0400 Subject: [PATCH 18/83] Trim version support matrix prose Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index f8cabe88d..4bcd22c69 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -13,9 +13,7 @@ Maintaining readiness before an incident occurs reduces response time and errors ### 1.1 Version Support Matrix Security fixes are applied to the **latest stable release only**. Users on older versions -are expected to upgrade. This is consistent with Pillow's quarterly release cadence and -is not currently documented elsewhere — reporters should assume only the latest release -will receive a patch. +are expected to upgrade. Reporters should assume only the latest release will receive a patch. | Branch | Status | |---|---| From 00ff8636a27f6d25d4abb8ea52040356df77c8a1 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 18:01:08 -0400 Subject: [PATCH 19/83] Remove section 7.5 Rollback Procedures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 4bcd22c69..c879e4ed9 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -168,23 +168,7 @@ For Critical and High severity where distro pre-notification improves user safet - Publish the GitHub Security Advisory. - Announce on [Mastodon](https://fosstodon.org/@pillow). -### 7.5 Rollback Procedures - -If a security patch introduces a critical regression after release: - -1. **Yank the release immediately** via the PyPI web interface: - [https://pypi.org/manage/project/pillow/](https://pypi.org/manage/project/pillow/) - (navigate to the release, click **"Yank"**). - Yanked releases remain downloadable by pinned users but are excluded from `pip install` - resolution, giving time to fix without leaving users unpatched. -2. Post a public notice in the GitHub release and on Mastodon explaining the regression and - that the release has been yanked. -3. If the previous (vulnerable) version was also yanked, **un-yank it temporarily** so users - have a functional fallback while the corrected release is prepared. -4. Prepare a corrected point release (incrementing the patch version), repeating sections 7.2–7.3. -5. Document the regression in the post-incident review (Section 9). - -### 7.6 Supply-Chain / Infrastructure Compromise +### 7.5 Supply-Chain / Infrastructure Compromise 1. **Immediately** revoke any potentially compromised credentials: - PyPI API tokens (regenerate and update in GitHub secrets) @@ -199,12 +183,12 @@ If a security patch introduces a critical regression after release: 4. Notify GitHub Security if repository access or Actions secrets are involved. 5. Issue a public advisory describing the scope and any user action required. -### 7.7 Recovery +### 7.6 Recovery After the fix is released and the advisory is public: 1. Verify that the patched wheels are live on PyPI and passing CI across all supported platforms. -2. Confirm any yanked releases are handled correctly (re-yank if un-yanked as a fallback during rollback). +2. Confirm any yanked releases are handled correctly . 3. Resume normal development operations on `main`. 4. Monitor the GitHub issue tracker and Mastodon for user reports of residual problems for at least **72 hours** post-release. 5. Close the private GitHub Security Advisory once recovery is confirmed. From 0d440b7d09b490701f5edf3f0f076bbfdef23afc Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 18:04:00 -0400 Subject: [PATCH 20/83] Trim Plan Maintenance section Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index c879e4ed9..029a69028 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -221,31 +221,7 @@ After the fix is released and the advisory is public: --- -## 9. Post-Incident Review - -Within **2 weeks** of a Critical or High severity fix being released: - -1. Hold a brief retrospective (async is fine for a distributed team). -2. Document the following metrics for the incident record: - - | Metric | Target | Actual | - |---|---|---| - | Time to acknowledge reporter | ≤ 72 hours | | - | Time to reproduce & assess severity | ≤ 5 days | | - | Time to develop & review fix | Varies by severity | | - | Time from report to public release | Critical ≤ 14 days; High ≤ 30 days | | - -3. Record: - - What went well - - What could be improved - - Root cause: what allowed the vulnerability to exist - - Whether any distro/downstream was impacted before the fix was available -4. File follow-up issues for any process improvements identified. -5. Update this document if the response process needs revision. - ---- - -## 10. Dependency Map +## 9. Dependency Map Understanding what Pillow depends on (upstream) and what depends on Pillow (downstream) is essential for scoping impact and coordinating notifications during an incident. @@ -348,10 +324,6 @@ This document is a living record. It should be kept current so it is useful when incident actually occurs. - **Quarterly review** — revisit during the Section 1.3 readiness review at each quarterly release. -- **Post-incident update** — if the response process revealed gaps or needed improvisation, - update this document before the post-incident review is closed (Section 9). -- **Ownership** — changes are approved by the Core Team and recorded in Git history. - Substantive changes should be noted in the PR description so they are easy to find later. --- From 80a91fdb4e90b609e1ad78a3df4cb70cd191be46 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 18:08:44 -0400 Subject: [PATCH 21/83] Add setuptools to Python-level dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 029a69028..b82889503 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -259,6 +259,7 @@ require a Pillow point release even if Pillow's own code is unchanged. | Package | Required? | Purpose | |---|---|---| +| `setuptools` | Build-time only | Package build backend | | `pybind11` | Build-time only | C++ ↔ Python bindings | | `olefile` | Optional (`fpx`, `mic` extras) | OLE2 container parsing (FPX, MIC formats) | | `defusedxml` | Optional (`xmp` extra) | Safe XML parsing for XMP metadata | From 6f815c2d8d088b2d1f87621a7a0d347a5a9052f4 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 18:13:43 -0400 Subject: [PATCH 22/83] Clarify advisory thread purpose as reporter coordination Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index b82889503..e4473ad33 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -198,7 +198,7 @@ After the fix is released and the advisory is public: ## 8. Communication ### Internal (during embargo) -- Use the **private GitHub Security Advisory** thread for all coordination. +- Use the **private GitHub Security Advisory** thread for coordination with the reporter. - Do not discuss details in public issues, PRs, or Gitter/IRC channels. ### External (at or after disclosure) From b579577aa0facc8bb03737a8741f81388200f6b6 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 18:15:25 -0400 Subject: [PATCH 23/83] Link to section 1.3 in Plan Maintenance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index e4473ad33..3eec4d4e4 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -324,7 +324,7 @@ When a CVE is published for a bundled C library: This document is a living record. It should be kept current so it is useful when an incident actually occurs. -- **Quarterly review** — revisit during the Section 1.3 readiness review at each quarterly release. +- **Quarterly review** — revisit during the [Section 1.3 readiness review](#13-readiness-review) at each quarterly release. --- From 55989595eaf4774347bec6f147d43a7cd5eb275f Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 18:17:39 -0400 Subject: [PATCH 24/83] Add private channels note to internal communication guidance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 3eec4d4e4..7e556728a 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -199,6 +199,7 @@ After the fix is released and the advisory is public: ### Internal (during embargo) - Use the **private GitHub Security Advisory** thread for coordination with the reporter. +- Use private communication channels for all other coordination. - Do not discuss details in public issues, PRs, or Gitter/IRC channels. ### External (at or after disclosure) From 6fe81dd52e34df4cbfa1b355996e565cb6df9802 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Thu, 9 Apr 2026 18:19:22 -0400 Subject: [PATCH 25/83] Remove Wand from downstream dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 7e556728a..e02127cb6 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -297,7 +297,6 @@ warrant proactive notification. | [Plone](https://plone.org/) | CMS image handling | | [Jupyter / IPython](https://jupyter.org/) | Inline image display | | [ReportLab](https://www.reportlab.com/) | PDF image embedding | -| [Wand](https://docs.wand-py.org/) | Sometimes used alongside Pillow | | [Tidelift subscribers](https://tidelift.com/) | Enterprise consumers (coordinated via Tidelift) | #### Pillow ecosystem plugins From 6a0192a40afb9de0869331f3ea439190ea819436 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Fri, 10 Apr 2026 07:40:27 -0400 Subject: [PATCH 26/83] Update .github/INCIDENT_RESPONSE.md Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index e02127cb6..f2fd90aa7 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -218,7 +218,7 @@ After the fix is released and the advisory is public: - Fixed version(s) - Nature of the vulnerability (without full exploit details if still fresh) - Credit to the reporter (with their consent) -- Upgrade instructions (`pip install --upgrade Pillow`) +- Upgrade instructions (`python3 -m pip install --upgrade Pillow`) --- @@ -278,7 +278,7 @@ these downstream consumers when assessing severity and planning communications. | Fedora / RHEL / CentOS | `python3-pillow` | [Red Hat Security](https://access.redhat.com/security/) | | Alpine Linux | `py3-pillow` | [Alpine security](https://security.alpinelinux.org/) | | Arch Linux | `python-pillow` | [Arch security tracker](https://security.archlinux.org/) | -| Homebrew (macOS) | `pillow` | [Homebrew maintainers](https://github.com/Homebrew/homebrew-core) | +| Homebrew | `pillow` | [Homebrew maintainers](https://github.com/Homebrew/homebrew-core) | | conda-forge | `pillow` | [conda-forge](https://github.com/conda-forge/pillow-feedstock) | #### Major Python ecosystem consumers @@ -411,7 +411,7 @@ incident actually occurs. > > **Remediation:** > ``` -> pip install --upgrade Pillow +> python3 -m pip install --upgrade Pillow > ``` > > **Timeline:** From d016c90108ae58610f5f159bdcfc2a537cb8409b Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Fri, 10 Apr 2026 07:43:12 -0400 Subject: [PATCH 27/83] Remove active exploitation escalation bullet from incident response Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index f2fd90aa7..574647f3e 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -124,7 +124,6 @@ Vulnerabilities and incidents may be reported or discovered through: Numbering Authority — no separate MITRE form required). The CVE is reserved privately and published automatically when the advisory goes public. 6. **Escalation** — Escalate beyond the core maintainer team if any of the following apply: - - The vulnerability is being actively exploited in the wild → notify [GitHub Security](mailto:security@github.com) and the [Python Security Response Team](https://www.python.org/news/security/) - The fix requires changes to CPython or a dependency outside Pillow's control → contact the relevant upstream immediately - A legal concern arises (e.g. GDPR-reportable data exposure) → contact the project's legal/fiscal sponsor - The Incident Lead is unreachable for > 24 hours on a Critical issue → any other maintainer may assume the role From 24b12dc84f77a6976c97f8b0992ea2e9d369d0ae Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Fri, 10 Apr 2026 07:49:37 -0400 Subject: [PATCH 28/83] Combine plan maintenance into a single paragraph Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 574647f3e..636ce72e8 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -320,10 +320,7 @@ When a CVE is published for a bundled C library: ## 11. Plan Maintenance -This document is a living record. It should be kept current so it is useful when an -incident actually occurs. - -- **Quarterly review** — revisit during the [Section 1.3 readiness review](#13-readiness-review) at each quarterly release. +This document is a living record. It should be kept current so it is useful when an incident actually occurs. Revisit it during the [Section 1.3 readiness review](#13-readiness-review) at each quarterly release. --- From 0cbdd2eff94f02e68e76dc9d5ec7d6060959dcea Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Fri, 10 Apr 2026 10:37:34 -0400 Subject: [PATCH 29/83] Update .github/INCIDENT_RESPONSE.md Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 636ce72e8..f3283ac3a 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -133,11 +133,7 @@ Vulnerabilities and incidents may be reported or discovered through: 1. Develop the fix in a **private fork** or directly in the private security advisory workspace on GitHub. Do **not** push to a public branch before the embargo lifts. 2. Write a regression test that fails before the fix and passes after. -3. Run the full test suite locally across all supported Python versions: - ```bash - make release-test - ``` -4. Review the patch with at least one other maintainer. +3. Review the patch with at least one other maintainer. ### 7.3 Standard (Non-Embargoed) Release From 6e1ccab749e6160cbecca0cccad068af3fead2da Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Fri, 10 Apr 2026 10:58:43 -0400 Subject: [PATCH 30/83] Address review feedback on INCIDENT_RESPONSE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update CVSS v3.1 to CVSS 4.0 throughout - Remove 'Direct maintainer contact' from detection sources - Fix 'before it stays public' wording for user bug reports - Simplify sections 7.3 and 7.4 to reference RELEASING.md instead of duplicating release process steps - Update RELEASING.md Point release section with security-specific steps (amend CVE in commits, publish GitHub Security Advisory) - Fix PyPI API tokens entry (remove GitHub secrets reference) - Fix 404 PyPI manage URL (use correct case and /releases/ path) - Replace security@pypi.org mailto with https://pypi.org/security/ - Remove unconfirmed 'Notify GitHub Security' bullet - Fix section numbering: 10.x → 9.x under Section 9. Dependency Map - Reorder: move 9.3 Responding to Upstream Vulnerability before 9.3 Downstream Dependencies (now 9.2 and 9.3 respectively) - Add anchor link for Section 5 reference in 9.2 - Add #plugin-list anchor to third-party plugins handbook link - Fix GitLab issue tracker URLs to use /-/work_items for libtiff, freetype2, and bzip2 - Add pyproject.toml reference for complete optional dependencies list Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 71 ++++++++++++++++++------------------ RELEASING.md | 2 + 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index f3283ac3a..b3c11a1df 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -76,7 +76,7 @@ One person may fill multiple roles. ## 5. Severity Classification -Use the [CVSS v3.1](https://www.first.org/cvss/v3.1/specification-document) base score as +Use the [CVSS 4.0](https://www.first.org/cvss/v4.0/specification-document) base score as a guide, mapped to the following levels: | Severity | CVSS | Definition | Target Response SLA | @@ -98,11 +98,10 @@ Vulnerabilities and incidents may be reported or discovered through: 1. **GitHub private security advisory** — preferred channel; see [SECURITY.md](SECURITY.md) 2. **Tidelift security contact** — -3. **Direct maintainer contact** — DM on Mastodon or email -4. **External researcher / coordinated disclosure** — e.g. Google Project Zero, vendor PSIRT -5. **Automated scanning** — Dependabot, GitHub code-scanning (CodeQL), CI fuzzing -6. **Distro security teams** — Debian, Red Hat, Ubuntu, Alpine may report upstream -7. **User bug report** — public issue (reassess if it has security implications before it stays public) +3. **External researcher / coordinated disclosure** — e.g. Google Project Zero, vendor PSIRT +4. **Automated scanning** — Dependabot, GitHub code-scanning (CodeQL), CI fuzzing +5. **Distro security teams** — Debian, Red Hat, Ubuntu, Alpine may report upstream +6. **User bug report** — public issue (reassess if it has security implications and convert to a private advisory if needed) --- @@ -142,9 +141,9 @@ For Medium and Low severity, or when no distro pre-notification is needed: 1. Merge the fix to `main`, then cherry-pick to all affected release branches (see [RELEASING.md — Point release](../RELEASING.md)). 2. Amend commit messages to include the CVE identifier. -3. Tag and push; the GitHub Actions "Wheels" workflow will build and upload to PyPI. +3. Follow the [Point release](../RELEASING.md#point-release) process in RELEASING.md to + tag, push, and confirm wheels are live on PyPI. 4. Publish the GitHub Security Advisory (this simultaneously publishes the CVE). -5. Announce on [Mastodon](https://fosstodon.org/@pillow). ### 7.4 Embargoed Release @@ -158,25 +157,24 @@ For Critical and High severity where distro pre-notification improves user safet or directly to individual distro security teams. 4. On the embargo date: - Amend commit messages with the CVE identifier. - - Tag and push all affected release branches (see [RELEASING.md — Embargoed release](../RELEASING.md)). - - Confirm the "Wheels" workflow has passed and wheels are live on PyPI. + - Follow the [Embargoed release](../RELEASING.md#embargoed-release) process in + RELEASING.md to tag, push, and confirm wheels are live on PyPI. - Publish the GitHub Security Advisory. - - Announce on [Mastodon](https://fosstodon.org/@pillow). ### 7.5 Supply-Chain / Infrastructure Compromise 1. **Immediately** revoke any potentially compromised credentials: - - PyPI API tokens (regenerate and update in GitHub secrets) + - PyPI API tokens - GitHub personal access tokens and OAuth apps - Codecov or other CI service tokens 2. Audit recent commits and releases for tampering: - Verify release tags against known-good SHAs - Re-inspect any wheel published since the potential compromise window -3. If a PyPI release is suspected to be tampered: yank it immediately via - [https://pypi.org/manage/project/pillow/](https://pypi.org/manage/project/pillow/); - file a report with the [PyPI security team](mailto:security@pypi.org). -4. Notify GitHub Security if repository access or Actions secrets are involved. -5. Issue a public advisory describing the scope and any user action required. +3. If a PyPI release is suspected to be tampered: yank it immediately via the + [PyPI release management page](https://pypi.org/manage/project/Pillow/releases/) + (login required); see [https://pypi.org/security/](https://pypi.org/security/) for + reporting to the PyPI security team. +4. Issue a public advisory describing the scope and any user action required. ### 7.6 Recovery @@ -222,7 +220,7 @@ After the fix is released and the advisory is public: Understanding what Pillow depends on (upstream) and what depends on Pillow (downstream) is essential for scoping impact and coordinating notifications during an incident. -### 10.1 Upstream Dependencies +### 9.1 Upstream Dependencies #### Bundled C libraries (shipped in official wheels) @@ -233,20 +231,20 @@ require a Pillow point release even if Pillow's own code is unchanged. |---|---|---| | [libjpeg-turbo](https://libjpeg-turbo.org/) | JPEG encode/decode | [GitHub](https://github.com/libjpeg-turbo/libjpeg-turbo/security) | | [libpng](http://www.libpng.org/pub/png/libpng.html) | PNG encode/decode | [SourceForge](https://sourceforge.net/p/libpng/bugs/) | -| [libtiff](https://libtiff.gitlab.io/libtiff/) | TIFF encode/decode | [GitLab](https://gitlab.com/libtiff/libtiff/-/issues) | +| [libtiff](https://libtiff.gitlab.io/libtiff/) | TIFF encode/decode | [GitLab](https://gitlab.com/libtiff/libtiff/-/work_items) | | [libwebp](https://chromium.googlesource.com/webm/libwebp) | WebP encode/decode | [Chromium tracker](https://bugs.chromium.org/p/webm/) | | [libavif](https://github.com/AOMediaCodec/libavif) | AVIF encode/decode | [GitHub](https://github.com/AOMediaCodec/libavif/security) | | [aom](https://aomedia.googlesource.com/aom/) | AV1 codec (AVIF) | [Chromium tracker](https://bugs.chromium.org/p/aomedia/) | | [dav1d](https://code.videolan.org/videolan/dav1d) | AV1 decode (AVIF) | [VideoLAN Security](https://www.videolan.org/security/) | | [openjpeg](https://www.openjpeg.org/) | JPEG 2000 encode/decode | [GitHub](https://github.com/uclouvain/openjpeg/security) | -| [freetype2](https://freetype.org/) | Font rendering | [GitLab](https://gitlab.freedesktop.org/freetype/freetype/-/issues) | +| [freetype2](https://freetype.org/) | Font rendering | [GitLab](https://gitlab.freedesktop.org/freetype/freetype/-/work_items) | | [lcms2](https://www.littlecms.com/) | ICC color management | [GitHub](https://github.com/mm2/Little-CMS) | | [harfbuzz](https://harfbuzz.github.io/) | Text shaping (via raqm) | [GitHub](https://github.com/harfbuzz/harfbuzz/security) | | [raqm](https://github.com/HOST-Oman/libraqm) | Complex text layout | [GitHub](https://github.com/HOST-Oman/libraqm) | | [fribidi](https://github.com/fribidi/fribidi) | Unicode bidi (via raqm) | [GitHub](https://github.com/fribidi/fribidi) | | [zlib](https://zlib.net/) | Deflate compression | [zlib.net](https://zlib.net/) | | [liblzma / xz-utils](https://tukaani.org/xz/) | XZ/LZMA compression | [GitHub](https://github.com/tukaani-project/xz) | -| [bzip2](https://gitlab.com/bzip2/bzip2) | BZ2 compression | [GitLab](https://gitlab.com/bzip2/bzip2/-/issues) | +| [bzip2](https://gitlab.com/bzip2/bzip2) | BZ2 compression | [GitLab](https://gitlab.com/bzip2/bzip2/-/work_items) | | [zstd](https://github.com/facebook/zstd) | Zstandard compression | [GitHub](https://github.com/facebook/zstd/security) | | [brotli](https://github.com/google/brotli) | Brotli compression | [GitHub](https://github.com/google/brotli) | | [libyuv](https://chromium.googlesource.com/libyuv/libyuv/) | YUV conversion | [Chromium tracker](https://bugs.chromium.org/p/libyuv/) | @@ -260,7 +258,21 @@ require a Pillow point release even if Pillow's own code is unchanged. | `olefile` | Optional (`fpx`, `mic` extras) | OLE2 container parsing (FPX, MIC formats) | | `defusedxml` | Optional (`xmp` extra) | Safe XML parsing for XMP metadata | -### 10.2 Downstream Dependencies +See [`pyproject.toml`](../pyproject.toml) for the complete and authoritative list of +optional dependencies. + +### 9.2 Responding to an Upstream Vulnerability + +When a CVE is published for a bundled C library: + +1. Assess whether the vulnerable code path is reachable through Pillow's API. +2. If reachable, treat as a Pillow vulnerability and follow [Section 5: Severity Classification](#5-severity-classification). +3. Update the bundled library version in the wheel build scripts and rebuild wheels. +4. Reference the upstream CVE in Pillow's release notes and GitHub Security Advisory. +5. If not reachable, document the rationale in a public issue so downstream distributors + can make informed decisions about patching their system packages. + +### 9.3 Downstream Dependencies A vulnerability in Pillow can have wide impact. Notify or consider the blast radius of these downstream consumers when assessing severity and planning communications. @@ -299,18 +311,7 @@ warrant proactive notification. Third-party plugins extend Pillow and are distributed separately on PyPI. Their maintainers should be notified for Critical/High issues that affect the plugin API or the formats they decode. See the -[full plugin list](https://pillow.readthedocs.io/en/stable/handbook/third-party-plugins.html). - -### 10.3 Responding to an Upstream Vulnerability - -When a CVE is published for a bundled C library: - -1. Assess whether the vulnerable code path is reachable through Pillow's API. -2. If reachable, treat as a Pillow vulnerability and follow Section 5. -3. Update the bundled library version in the wheel build scripts and rebuild wheels. -4. Reference the upstream CVE in Pillow's release notes and GitHub Security Advisory. -5. If not reachable, document the rationale in a public issue so downstream distributors - can make informed decisions about patching their system packages. +[full plugin list](https://pillow.readthedocs.io/en/stable/handbook/third-party-plugins.html#plugin-list). --- @@ -328,7 +329,7 @@ This document is a living record. It should be kept current so it is useful when - [Tidelift Security Contact](https://tidelift.com/security) - [GitHub: Privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability) - [GitHub as a CVE Numbering Authority (CNA)](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/about-repository-security-advisories) -- [FIRST CVSS v3.1 Calculator](https://www.first.org/cvss/calculator/3.1) +- [FIRST CVSS 4.0 Calculator](https://www.first.org/cvss/calculator/4.0) - [linux-distros mailing list](https://oss-security.openwall.org/wiki/mailing-lists/distros) - [OpenSSF CVD Guide](https://github.com/ossf/oss-vulnerability-guide) *(basis for this plan)* diff --git a/RELEASING.md b/RELEASING.md index 3c6188c82..469dca62a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -19,6 +19,7 @@ Released as needed for security, installation or critical bug fixes. git checkout -t remotes/origin/5.2.x ``` * [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. +* [ ] If this is a security fix: amend commits to include the CVE identifier in the commit message. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in release branch e.g. `5.2.x`. * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Run pre-release check via `make release-test`. @@ -38,6 +39,7 @@ Released as needed for security, installation or critical bug fixes. ```bash git push ``` +* [ ] If this is a security fix: publish the [GitHub Security Advisory](https://github.com/python-pillow/Pillow/security/advisories). ## Embargoed release From a49c63208a1e6a7a8c13b9ee8337e9ef5449054e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:38:35 +0200 Subject: [PATCH 31/83] Move dependency versions to single JSON and enable Renovate --- .github/dependencies.json | 19 +++ .github/renovate.json | 163 ++++++++++++++++++++++- .github/workflows/cifuzz.yml | 2 + .github/workflows/wheels-dependencies.sh | 33 ++--- .github/workflows/wheels.yml | 2 + winbuild/build_prepare.py | 35 +++-- 6 files changed, 218 insertions(+), 36 deletions(-) create mode 100644 .github/dependencies.json diff --git a/.github/dependencies.json b/.github/dependencies.json new file mode 100644 index 000000000..0f61b7817 --- /dev/null +++ b/.github/dependencies.json @@ -0,0 +1,19 @@ +{ + "brotli": "1.2.0", + "bzip2": "1.0.8", + "freetype": "2.14.3", + "fribidi": "1.0.16", + "harfbuzz": "13.2.1", + "jpegturbo": "3.1.4.1", + "lcms2": "2.18", + "libavif": "1.4.1", + "libimagequant": "4.4.1", + "libpng": "1.6.56", + "libwebp": "1.6.0", + "libxcb": "1.17.0", + "openjpeg": "2.5.4", + "tiff": "4.7.1", + "xz": "5.8.3", + "zlib-ng": "2.3.3", + "zstd": "1.5.7" +} diff --git a/.github/renovate.json b/.github/renovate.json index 8187fc15b..212959be6 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -7,16 +7,167 @@ "Dependency" ], "minimumReleaseAge": "7 days", + "schedule": [ + "* * 3 * *" + ], + "customManagers": [ + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"brotli\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "brotli", + "packageNameTemplate": "google/brotli", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"fribidi\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "fribidi", + "packageNameTemplate": "fribidi/fribidi", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"harfbuzz\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "harfbuzz", + "packageNameTemplate": "harfbuzz/harfbuzz", + "datasourceTemplate": "github-releases" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"jpegturbo\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "jpegturbo", + "packageNameTemplate": "libjpeg-turbo/libjpeg-turbo", + "datasourceTemplate": "github-releases" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"lcms2\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "lcms2", + "packageNameTemplate": "mm2/Little-CMS", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^lcms(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libavif\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "libavif", + "packageNameTemplate": "AOMediaCodec/libavif", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libimagequant\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "libimagequant", + "packageNameTemplate": "ImageOptim/libimagequant", + "datasourceTemplate": "github-tags" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libwebp\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "libwebp", + "packageNameTemplate": "webmproject/libwebp", + "datasourceTemplate": "github-tags", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"openjpeg\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "openjpeg", + "packageNameTemplate": "uclouvain/openjpeg", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"tiff\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "tiff", + "packageNameTemplate": "libsdl-org/libtiff", + "datasourceTemplate": "github-tags", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"xz\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "xz", + "packageNameTemplate": "tukaani-project/xz", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"zlib-ng\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "zlib-ng", + "packageNameTemplate": "zlib-ng/zlib-ng", + "datasourceTemplate": "github-releases" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"zstd\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "zstd", + "packageNameTemplate": "facebook/zstd", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"freetype\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "freetype", + "packageNameTemplate": "freetype/freetype", + "datasourceTemplate": "github-tags", + "extractVersionTemplate": "^VER-(?[\\d-]+)$", + "versioningTemplate": "regex:^(?\\d+)[.-](?\\d+)[.-](?\\d+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libpng\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "libpng", + "packageNameTemplate": "pnggroup/libpng", + "datasourceTemplate": "github-tags", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libxcb\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "libxcb", + "packageNameTemplate": "xorg/lib/libxcb", + "datasourceTemplate": "gitlab-tags", + "registryUrlTemplate": "https://gitlab.freedesktop.org", + "extractVersionTemplate": "^libxcb-(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"bzip2\":\\s*\"(?\\d+[^\"]*?)\""], + "depNameTemplate": "bzip2", + "packageNameTemplate": "bzip2/bzip2", + "datasourceTemplate": "gitlab-tags", + "extractVersionTemplate": "^bzip2-(?.+)$" + } + ], "packageRules": [ { "groupName": "github-actions", - "matchManagers": [ - "github-actions" - ], + "matchManagers": ["github-actions"], "separateMajorMinor": false } - ], - "schedule": [ - "* * 3 * *" ] } diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 263700780..b92e88e06 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -5,12 +5,14 @@ on: branches: - "**" paths: + - ".github/dependencies.json" - ".github/workflows/cifuzz.yml" - ".github/workflows/wheels-dependencies.sh" - "**.c" - "**.h" pull_request: paths: + - ".github/dependencies.json" - ".github/workflows/cifuzz.yml" - ".github/workflows/wheels-dependencies.sh" - "**.c" diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 7750a2e07..12593b3f5 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -89,22 +89,23 @@ fi ARCHIVE_SDIR=pillow-depends-main -# Package versions for fresh source builds. -FREETYPE_VERSION=2.14.3 -HARFBUZZ_VERSION=13.2.1 -LIBPNG_VERSION=1.6.56 -JPEGTURBO_VERSION=3.1.4.1 -OPENJPEG_VERSION=2.5.4 -XZ_VERSION=5.8.3 -ZSTD_VERSION=1.5.7 -TIFF_VERSION=4.7.1 -LCMS2_VERSION=2.18 -ZLIB_NG_VERSION=2.3.3 -LIBWEBP_VERSION=1.6.0 -BZIP2_VERSION=1.0.8 -LIBXCB_VERSION=1.17.0 -BROTLI_VERSION=1.2.0 -LIBAVIF_VERSION=1.4.1 +VERSIONS_FILE="$PROJECTDIR/.github/dependencies.json" +_get_ver() { python3 -c "import json; print(json.load(open('$VERSIONS_FILE'))['$1'])"; } +FREETYPE_VERSION=$(_get_ver freetype) +HARFBUZZ_VERSION=$(_get_ver harfbuzz) +LIBPNG_VERSION=$(_get_ver libpng) +JPEGTURBO_VERSION=$(_get_ver jpegturbo) +OPENJPEG_VERSION=$(_get_ver openjpeg) +XZ_VERSION=$(_get_ver xz) +ZSTD_VERSION=$(_get_ver zstd) +TIFF_VERSION=$(_get_ver tiff) +LCMS2_VERSION=$(_get_ver lcms2) +ZLIB_NG_VERSION=$(_get_ver zlib-ng) +LIBWEBP_VERSION=$(_get_ver libwebp) +BZIP2_VERSION=$(_get_ver bzip2) +LIBXCB_VERSION=$(_get_ver libxcb) +BROTLI_VERSION=$(_get_ver brotli) +LIBAVIF_VERSION=$(_get_ver libavif) function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index a786f9939..415f7eb29 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -12,6 +12,7 @@ on: push: paths: - ".ci/requirements-cibw.txt" + - ".github/dependencies.json" - ".github/workflows/wheel*" - "pyproject.toml" - "setup.py" @@ -23,6 +24,7 @@ on: pull_request: paths: - ".ci/requirements-cibw.txt" + - ".github/dependencies.json" - ".github/workflows/wheel*" - "pyproject.toml" - "setup.py" diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3b16da58a..f659479e6 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import json import os import platform import re @@ -8,6 +9,7 @@ import shutil import struct import subprocess import sys +from pathlib import Path from typing import Any @@ -112,21 +114,26 @@ ARCHITECTURES = { "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } +_versions = json.loads( + (Path(__file__).parents[1] / ".github" / "dependencies.json").read_text() +) + + V = { - "BROTLI": "1.2.0", - "FREETYPE": "2.14.3", - "FRIBIDI": "1.0.16", - "HARFBUZZ": "13.2.1", - "JPEGTURBO": "3.1.4.1", - "LCMS2": "2.18", - "LIBAVIF": "1.4.1", - "LIBIMAGEQUANT": "4.4.1", - "LIBPNG": "1.6.56", - "LIBWEBP": "1.6.0", - "OPENJPEG": "2.5.4", - "TIFF": "4.7.1", - "XZ": "5.8.3", - "ZLIBNG": "2.3.3", + "BROTLI": _versions["brotli"], + "FREETYPE": _versions["freetype"], + "FRIBIDI": _versions["fribidi"], + "HARFBUZZ": _versions["harfbuzz"], + "JPEGTURBO": _versions["jpegturbo"], + "LCMS2": _versions["lcms2"], + "LIBAVIF": _versions["libavif"], + "LIBIMAGEQUANT": _versions["libimagequant"], + "LIBPNG": _versions["libpng"], + "LIBWEBP": _versions["libwebp"], + "OPENJPEG": _versions["openjpeg"], + "TIFF": _versions["tiff"], + "XZ": _versions["xz"], + "ZLIBNG": _versions["zlib-ng"], } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From 65767a0cf7502dbf92fd34085514508802d07373 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:08:07 +0300 Subject: [PATCH 32/83] Use GitLab as data source for libtiff Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/renovate.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 212959be6..80c352139 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -94,8 +94,8 @@ "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], "matchStrings": ["\"tiff\":\\s*\"(?\\d+[^\"]*?)\""], "depNameTemplate": "tiff", - "packageNameTemplate": "libsdl-org/libtiff", - "datasourceTemplate": "github-tags", + "packageNameTemplate": "libtiff/libtiff", + "datasourceTemplate": "gitlab-tags", "extractVersionTemplate": "^v(?.+)$" }, { From 6dd03edba80dd10a00d8e7b2dfc19799732c3858 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:39:38 +0300 Subject: [PATCH 33/83] Use GitLab as data source for FreeType Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 80c352139..6bd1c080e 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -130,7 +130,8 @@ "matchStrings": ["\"freetype\":\\s*\"(?\\d+[^\"]*?)\""], "depNameTemplate": "freetype", "packageNameTemplate": "freetype/freetype", - "datasourceTemplate": "github-tags", + "datasourceTemplate": "gitlab-tags", + "registryUrlTemplate": "https://gitlab.freedesktop.org", "extractVersionTemplate": "^VER-(?[\\d-]+)$", "versioningTemplate": "regex:^(?\\d+)[.-](?\\d+)[.-](?\\d+)$" }, From ee24a1107393d5763a14256fb665303b4634c9e8 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 14 Apr 2026 11:15:08 -0400 Subject: [PATCH 34/83] Update .github/INCIDENT_RESPONSE.md Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 2 +- RELEASING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index b3c11a1df..e05673a5d 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -254,7 +254,7 @@ require a Pillow point release even if Pillow's own code is unchanged. | Package | Required? | Purpose | |---|---|---| | `setuptools` | Build-time only | Package build backend | -| `pybind11` | Build-time only | C++ ↔ Python bindings | +| `pybind11` | Build-time only | Compile C files in parallel | | `olefile` | Optional (`fpx`, `mic` extras) | OLE2 container parsing (FPX, MIC formats) | | `defusedxml` | Optional (`xmp` extra) | Safe XML parsing for XMP metadata | diff --git a/RELEASING.md b/RELEASING.md index 469dca62a..fcf108943 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -39,7 +39,7 @@ Released as needed for security, installation or critical bug fixes. ```bash git push ``` -* [ ] If this is a security fix: publish the [GitHub Security Advisory](https://github.com/python-pillow/Pillow/security/advisories). +* [ ] If this is a security fix: publish the [GitHub Security Advisory or Advisories](https://github.com/python-pillow/Pillow/security/advisories). ## Embargoed release From a124ed208f84b3e12c9e9cb8f8006a740783baad Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 14 Apr 2026 11:36:33 -0400 Subject: [PATCH 35/83] Update template wording --- .github/INCIDENT_RESPONSE.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index e05673a5d..0cefea6cb 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -343,21 +343,23 @@ This document is a living record. It should be kept current so it is useful when > > Hi \, > -> Thank you for taking the time to report this — we genuinely appreciate it. +> Thank you for taking the time to report this issue. We appreciate it. > -> We have received your report and will assess it within the next few days. We will keep -> you updated on our progress. +> We have received your report and will review it as soon as possible. We will +> keep you updated on our progress. > -> A few quick questions so we can handle this well: -> - How would you like to be credited in the advisory? (name, handle, organisation, or anonymous) -> - Do you plan to publish your own write-up or advisory? If so, is there a disclosure date -> that works for you? +> Questions: > -> We aim to treat all vulnerability reports in line with coordinated disclosure principles. -> If you have any questions or concerns at any point, please reply to this thread. +> - How would you like to be credited in the advisory? (name, handle, +> organisation, or anonymous) +> - Do you plan to publish your own write-up or advisory? If so, do you have a +> disclosure date in mind? > -> Thanks again, -> The Pillow maintainers +> We apply coordinated disclosure principles to all vulnerability reports. If +> you have any questions or concerns at any point, please reply to this thread. +> +> Thank you again, +> The Pillow team ### A.2 Embargoed Distro Notification From 9f24881521e64310a9f888c66a8310599d20cac5 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 14 Apr 2026 12:13:45 -0400 Subject: [PATCH 36/83] 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 42ff1615b..2b668cc55 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 acdeff7db..23255eb8a 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 000000000..045b9410a --- /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 37/83] 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 045b9410a..ebff8199f 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 658d9ce258701a23d2a1a1270e88fef42a4f377c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Apr 2026 19:52:16 +1000 Subject: [PATCH 38/83] Updated wheels path regex --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 415f7eb29..b5edfc461 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -13,7 +13,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/dependencies.json" - - ".github/workflows/wheel*" + - ".github/workflows/wheels*" - "pyproject.toml" - "setup.py" - "wheels/*" @@ -25,7 +25,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/dependencies.json" - - ".github/workflows/wheel*" + - ".github/workflows/wheels*" - "pyproject.toml" - "setup.py" - "wheels/*" From ff00aaa6d3382dc6e15dbc2b9e45463fe010d788 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Apr 2026 19:59:52 +1000 Subject: [PATCH 39/83] Use keys from dependencies JSON --- winbuild/build_prepare.py | 84 +++++++++++++++------------------------ 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f659479e6..f55c82112 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -114,35 +114,17 @@ ARCHITECTURES = { "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } -_versions = json.loads( +V = json.loads( (Path(__file__).parents[1] / ".github" / "dependencies.json").read_text() ) - - -V = { - "BROTLI": _versions["brotli"], - "FREETYPE": _versions["freetype"], - "FRIBIDI": _versions["fribidi"], - "HARFBUZZ": _versions["harfbuzz"], - "JPEGTURBO": _versions["jpegturbo"], - "LCMS2": _versions["lcms2"], - "LIBAVIF": _versions["libavif"], - "LIBIMAGEQUANT": _versions["libimagequant"], - "LIBPNG": _versions["libpng"], - "LIBWEBP": _versions["libwebp"], - "OPENJPEG": _versions["openjpeg"], - "TIFF": _versions["tiff"], - "XZ": _versions["xz"], - "ZLIBNG": _versions["zlib-ng"], -} -V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) +V["libpng-xy"] = "".join(V["libpng"].split(".")[:2]) # dependencies, listed in order of compilation DEPS: dict[str, dict[str, Any]] = { "libjpeg": { - "url": f"https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/{V['JPEGTURBO']}/libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", - "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", + "url": f"https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/{V['jpegturbo']}/libjpeg-turbo-{V['jpegturbo']}.tar.gz", + "filename": f"libjpeg-turbo-{V['jpegturbo']}.tar.gz", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" @@ -169,8 +151,8 @@ DEPS: dict[str, dict[str, Any]] = { "bins": ["djpeg.exe"], }, "zlib": { - "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.tar.gz", - "filename": f"zlib-ng-{V['ZLIBNG']}.tar.gz", + "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['zlib-ng']}.tar.gz", + "filename": f"zlib-ng-{V['zlib-ng']}.tar.gz", "license": "LICENSE.md", "patch": { r"CMakeLists.txt": { @@ -186,8 +168,8 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"zlib.lib"], }, "xz": { - "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME", - "filename": f"xz-{V['XZ']}.tar.gz", + "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['xz']}/FILENAME", + "filename": f"xz-{V['xz']}.tar.gz", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -199,7 +181,7 @@ DEPS: dict[str, dict[str, Any]] = { }, "libwebp": { "url": "http://downloads.webmproject.org/releases/webp/FILENAME", - "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", + "filename": f"libwebp-{V['libwebp']}.tar.gz", "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { @@ -220,7 +202,7 @@ DEPS: dict[str, dict[str, Any]] = { }, "libtiff": { "url": "https://download.osgeo.org/libtiff/FILENAME", - "filename": f"tiff-{V['TIFF']}.tar.gz", + "filename": f"tiff-{V['tiff']}.tar.gz", "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { @@ -244,22 +226,22 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"libtiff\*.lib"], }, "libpng": { - "url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/" + "url": f"{SF_PROJECTS}/libpng/files/libpng{V['libpng-xy']}/{V['libpng']}/" f"FILENAME/download", - "filename": f"libpng-{V['LIBPNG']}.tar.gz", + "filename": f"libpng-{V['libpng']}.tar.gz", "license": "LICENSE", "build": [ *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), cmd_copy( - f"libpng{V['LIBPNG_XY']}_static.lib", f"libpng{V['LIBPNG_XY']}.lib" + f"libpng{V['libpng-xy']}_static.lib", f"libpng{V['libpng-xy']}.lib" ), ], "headers": [r"png*.h"], - "libs": [f"libpng{V['LIBPNG_XY']}.lib"], + "libs": [f"libpng{V['libpng-xy']}.lib"], }, "brotli": { - "url": f"https://github.com/google/brotli/archive/refs/tags/v{V['BROTLI']}.tar.gz", - "filename": f"brotli-{V['BROTLI']}.tar.gz", + "url": f"https://github.com/google/brotli/archive/refs/tags/v{V['brotli']}.tar.gz", + "filename": f"brotli-{V['brotli']}.tar.gz", "license": "LICENSE", "build": [ *cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -269,7 +251,7 @@ DEPS: dict[str, dict[str, Any]] = { }, "freetype": { "url": "https://download.savannah.gnu.org/releases/freetype/FILENAME", - "filename": f"freetype-{V['FREETYPE']}.tar.gz", + "filename": f"freetype-{V['freetype']}.tar.gz", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { @@ -282,7 +264,7 @@ DEPS: dict[str, dict[str, Any]] = { "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ;FT_CONFIG_OPTION_USE_BROTLI", # noqa: E501 "": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501 "": "{lib_dir}", # noqa: E501 - "": f"zlib.lib;libpng{V['LIBPNG_XY']}.lib;brotlicommon.lib;brotlidec.lib", # noqa: E501 + "": f"zlib.lib;libpng{V['libpng-xy']}.lib;brotlicommon.lib;brotlidec.lib", # noqa: E501 }, r"src/autofit/afshaper.c": { # link against harfbuzz.lib @@ -302,8 +284,8 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], }, "lcms2": { - "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download", - "filename": f"lcms2-{V['LCMS2']}.tar.gz", + "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['lcms2']}/FILENAME/download", + "filename": f"lcms2-{V['lcms2']}.tar.gz", "license": "LICENSE", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { @@ -327,21 +309,21 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"Lib\MS\*.lib"], }, "openjpeg": { - "url": f"https://github.com/uclouvain/openjpeg/archive/v{V['OPENJPEG']}.tar.gz", - "filename": f"openjpeg-{V['OPENJPEG']}.tar.gz", + "url": f"https://github.com/uclouvain/openjpeg/archive/v{V['openjpeg']}.tar.gz", + "filename": f"openjpeg-{V['openjpeg']}.tar.gz", "license": "LICENSE", "build": [ *cmds_cmake( "openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF" ), - cmd_mkdir(rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"), - cmd_copy(r"src\lib\openjp2\*.h", rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"), + cmd_mkdir(rf"{{inc_dir}}\openjpeg-{V['openjpeg']}"), + cmd_copy(r"src\lib\openjp2\*.h", rf"{{inc_dir}}\openjpeg-{V['openjpeg']}"), ], "libs": [r"bin\*.lib"], }, "libimagequant": { - "url": "https://github.com/ImageOptim/libimagequant/archive/{V['LIBIMAGEQUANT']}.tar.gz", - "filename": f"libimagequant-{V['LIBIMAGEQUANT']}.tar.gz", + "url": "https://github.com/ImageOptim/libimagequant/archive/{V['libimagequant']}.tar.gz", + "filename": f"libimagequant-{V['libimagequant']}.tar.gz", "license": "COPYRIGHT", "build": [ cmd_cd("imagequant-sys"), @@ -351,8 +333,8 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"..\target\release\imagequant_sys.lib"], }, "harfbuzz": { - "url": f"https://github.com/harfbuzz/harfbuzz/releases/download/{V['HARFBUZZ']}/FILENAME", - "filename": f"harfbuzz-{V['HARFBUZZ']}.tar.xz", + "url": f"https://github.com/harfbuzz/harfbuzz/releases/download/{V['harfbuzz']}/FILENAME", + "filename": f"harfbuzz-{V['harfbuzz']}.tar.xz", "license": "COPYING", "build": [ *cmds_cmake( @@ -365,11 +347,11 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"*.lib"], }, "fribidi": { - "url": f"https://github.com/fribidi/fribidi/archive/v{V['FRIBIDI']}.zip", - "filename": f"fribidi-{V['FRIBIDI']}.zip", + "url": f"https://github.com/fribidi/fribidi/archive/v{V['fribidi']}.zip", + "filename": f"fribidi-{V['fribidi']}.zip", "license": "COPYING", "build": [ - cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['FRIBIDI']}-COPYING"), + cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['fribidi']}-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), # generated tab.i files cannot be cross-compiled " ^&^& ".join( @@ -383,8 +365,8 @@ DEPS: dict[str, dict[str, Any]] = { "bins": [r"*.dll"], }, "libavif": { - "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.tar.gz", - "filename": f"libavif-{V['LIBAVIF']}.tar.gz", + "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['libavif']}.tar.gz", + "filename": f"libavif-{V['libavif']}.tar.gz", "license": "LICENSE", "build": [ "rustup update", From 237ab0763c614a907389b4606db331dfcf45a89b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Apr 2026 20:25:25 +1000 Subject: [PATCH 40/83] Remove unneeded ? from matchStrings regex --- .github/renovate.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 6bd1c080e..da998d7de 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -14,7 +14,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"brotli\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"brotli\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "brotli", "packageNameTemplate": "google/brotli", "datasourceTemplate": "github-releases", @@ -23,7 +23,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"fribidi\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"fribidi\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "fribidi", "packageNameTemplate": "fribidi/fribidi", "datasourceTemplate": "github-releases", @@ -32,7 +32,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"harfbuzz\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"harfbuzz\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "harfbuzz", "packageNameTemplate": "harfbuzz/harfbuzz", "datasourceTemplate": "github-releases" @@ -40,7 +40,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"jpegturbo\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"jpegturbo\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "jpegturbo", "packageNameTemplate": "libjpeg-turbo/libjpeg-turbo", "datasourceTemplate": "github-releases" @@ -48,7 +48,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"lcms2\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"lcms2\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "lcms2", "packageNameTemplate": "mm2/Little-CMS", "datasourceTemplate": "github-releases", @@ -57,7 +57,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"libavif\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"libavif\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "libavif", "packageNameTemplate": "AOMediaCodec/libavif", "datasourceTemplate": "github-releases", @@ -66,7 +66,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"libimagequant\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"libimagequant\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "libimagequant", "packageNameTemplate": "ImageOptim/libimagequant", "datasourceTemplate": "github-tags" @@ -74,7 +74,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"libwebp\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"libwebp\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "libwebp", "packageNameTemplate": "webmproject/libwebp", "datasourceTemplate": "github-tags", @@ -83,7 +83,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"openjpeg\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"openjpeg\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "openjpeg", "packageNameTemplate": "uclouvain/openjpeg", "datasourceTemplate": "github-releases", @@ -92,7 +92,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"tiff\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"tiff\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "tiff", "packageNameTemplate": "libtiff/libtiff", "datasourceTemplate": "gitlab-tags", @@ -101,7 +101,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"xz\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"xz\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "xz", "packageNameTemplate": "tukaani-project/xz", "datasourceTemplate": "github-releases", @@ -110,7 +110,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"zlib-ng\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"zlib-ng\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "zlib-ng", "packageNameTemplate": "zlib-ng/zlib-ng", "datasourceTemplate": "github-releases" @@ -118,7 +118,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"zstd\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"zstd\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "zstd", "packageNameTemplate": "facebook/zstd", "datasourceTemplate": "github-releases", @@ -127,7 +127,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"freetype\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"freetype\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "freetype", "packageNameTemplate": "freetype/freetype", "datasourceTemplate": "gitlab-tags", @@ -138,7 +138,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"libpng\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"libpng\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "libpng", "packageNameTemplate": "pnggroup/libpng", "datasourceTemplate": "github-tags", @@ -147,7 +147,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"libxcb\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"libxcb\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "libxcb", "packageNameTemplate": "xorg/lib/libxcb", "datasourceTemplate": "gitlab-tags", @@ -157,7 +157,7 @@ { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"bzip2\":\\s*\"(?\\d+[^\"]*?)\""], + "matchStrings": ["\"bzip2\":\\s*\"(?\\d+[^\"]*)\""], "depNameTemplate": "bzip2", "packageNameTemplate": "bzip2/bzip2", "datasourceTemplate": "gitlab-tags", From b27ae0b2fd3f198dd9a703828162585e1ce11c24 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Apr 2026 20:57:03 +1000 Subject: [PATCH 41/83] Reorder to match dependencies order --- .github/renovate.json | 78 +++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index da998d7de..f5af3d05a 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -20,6 +20,26 @@ "datasourceTemplate": "github-releases", "extractVersionTemplate": "^v(?.+)$" }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"bzip2\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "bzip2", + "packageNameTemplate": "bzip2/bzip2", + "datasourceTemplate": "gitlab-tags", + "extractVersionTemplate": "^bzip2-(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"freetype\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "freetype", + "packageNameTemplate": "freetype/freetype", + "datasourceTemplate": "gitlab-tags", + "registryUrlTemplate": "https://gitlab.freedesktop.org", + "extractVersionTemplate": "^VER-(?[\\d-]+)$", + "versioningTemplate": "regex:^(?\\d+)[.-](?\\d+)[.-](?\\d+)$" + }, { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], @@ -71,6 +91,15 @@ "packageNameTemplate": "ImageOptim/libimagequant", "datasourceTemplate": "github-tags" }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libpng\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "libpng", + "packageNameTemplate": "pnggroup/libpng", + "datasourceTemplate": "github-tags", + "extractVersionTemplate": "^v(?.+)$" + }, { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], @@ -80,6 +109,16 @@ "datasourceTemplate": "github-tags", "extractVersionTemplate": "^v(?.+)$" }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libxcb\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "libxcb", + "packageNameTemplate": "xorg/lib/libxcb", + "datasourceTemplate": "gitlab-tags", + "registryUrlTemplate": "https://gitlab.freedesktop.org", + "extractVersionTemplate": "^libxcb-(?.+)$" + }, { "customType": "regex", "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], @@ -123,45 +162,6 @@ "packageNameTemplate": "facebook/zstd", "datasourceTemplate": "github-releases", "extractVersionTemplate": "^v(?.+)$" - }, - { - "customType": "regex", - "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"freetype\":\\s*\"(?\\d+[^\"]*)\""], - "depNameTemplate": "freetype", - "packageNameTemplate": "freetype/freetype", - "datasourceTemplate": "gitlab-tags", - "registryUrlTemplate": "https://gitlab.freedesktop.org", - "extractVersionTemplate": "^VER-(?[\\d-]+)$", - "versioningTemplate": "regex:^(?\\d+)[.-](?\\d+)[.-](?\\d+)$" - }, - { - "customType": "regex", - "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"libpng\":\\s*\"(?\\d+[^\"]*)\""], - "depNameTemplate": "libpng", - "packageNameTemplate": "pnggroup/libpng", - "datasourceTemplate": "github-tags", - "extractVersionTemplate": "^v(?.+)$" - }, - { - "customType": "regex", - "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"libxcb\":\\s*\"(?\\d+[^\"]*)\""], - "depNameTemplate": "libxcb", - "packageNameTemplate": "xorg/lib/libxcb", - "datasourceTemplate": "gitlab-tags", - "registryUrlTemplate": "https://gitlab.freedesktop.org", - "extractVersionTemplate": "^libxcb-(?.+)$" - }, - { - "customType": "regex", - "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], - "matchStrings": ["\"bzip2\":\\s*\"(?\\d+[^\"]*)\""], - "depNameTemplate": "bzip2", - "packageNameTemplate": "bzip2/bzip2", - "datasourceTemplate": "gitlab-tags", - "extractVersionTemplate": "^bzip2-(?.+)$" } ], "packageRules": [ From b300e788384e8fff6ac1fcc5f877599ebc24148a Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 14 Apr 2026 20:08:05 -0400 Subject: [PATCH 42/83] 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 ebff8199f..dc7e96c60 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 43/83] 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 2b668cc55..c9a396aa8 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 dc7e96c60..e018f099a 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 44/83] 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 e018f099a..ec91ea82c 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 45/83] 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 ec91ea82c..2984d8e2b 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 2593703e5122c9c60b441ac4c1fd868c6ddf8eeb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:07:21 +0300 Subject: [PATCH 46/83] Hash pin GitHub Actions --- .github/workflows/cifuzz.yml | 8 +++--- .github/workflows/docs.yml | 10 ++++---- .github/workflows/lint.yml | 6 ++--- .github/workflows/release-drafter.yml | 2 +- .github/workflows/stale.yml | 2 +- .github/workflows/test-docker.yml | 6 ++--- .github/workflows/test-mingw.yml | 4 +-- .github/workflows/test-valgrind-memory.yml | 2 +- .github/workflows/test-valgrind.yml | 2 +- .github/workflows/test-windows.yml | 14 +++++----- .github/workflows/test.yml | 14 +++++----- .github/workflows/wheels.yml | 30 +++++++++++----------- .github/zizmor.yml | 6 ----- 13 files changed, 50 insertions(+), 56 deletions(-) delete mode 100644 .github/zizmor.yml diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index b92e88e06..cc8b4606b 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -35,27 +35,27 @@ jobs: steps: - name: Build Fuzzers id: build - uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@e41e2f295eb18d630932fdd33d072527ba74c87b # master with: oss-fuzz-project-name: 'pillow' language: python dry-run: false - name: Run Fuzzers id: run - uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@e41e2f295eb18d630932fdd33d072527ba74c87b # master with: oss-fuzz-project-name: 'pillow' fuzz-seconds: 600 language: python dry-run: false - name: Upload New Crash - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts - name: Upload Legacy Crash - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: steps.run.outcome == 'success' with: name: crash diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 857881c01..8c29af7b7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,12 +32,12 @@ jobs: name: Docs steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" cache: pip @@ -49,21 +49,21 @@ jobs: run: python3 .github/workflows/system-info.py - name: Cache libavif - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libavif with: path: ~/cache-libavif key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }} - name: Cache libimagequant - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libimagequant with: path: ~/cache-libimagequant key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} - name: Cache libwebp - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libwebp with: path: ~/cache-libwebp diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e2f8bf47a..1aff5a0dd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,14 +18,14 @@ jobs: runs-on: ubuntu-latest name: Lint steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-python@v6 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 - name: Lint run: uvx --with tox-uv tox -e lint - name: Mypy diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index aa8326b78..d62d2c3c2 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -26,6 +26,6 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next release notes as pull requests are merged into "main" - - uses: release-drafter/release-drafter@v7 + - uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e4ccd1aa3..b2dca6dd2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v10 + uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 515d77d17..083cb9fc2 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -67,7 +67,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -76,7 +76,7 @@ jobs: - name: Set up QEMU if: "matrix.qemu-arch" - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: platforms: ${{ matrix.qemu-arch }} @@ -104,7 +104,7 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 0dc6e2a0c..a87928f0b 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -87,7 +87,7 @@ jobs: .ci/test.sh - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: files: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index 87eace643..1cbcc40d3 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -44,7 +44,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index f14dab616..f3ec8c10e 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -42,7 +42,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 0b2aad283..6a83338d8 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -49,19 +49,19 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Checkout cached dependencies - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: python-pillow/pillow-depends path: winbuild\depends - name: Checkout extra test images - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: python-pillow/test-images @@ -69,7 +69,7 @@ jobs: # sets env: pythonLocation - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -113,7 +113,7 @@ jobs: - name: Cache build id: build-cache - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: winbuild\build key: @@ -217,7 +217,7 @@ jobs: shell: bash - name: Upload errors - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() with: name: errors @@ -229,7 +229,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: files: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d84504a8f..2654e2d04 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,12 +69,12 @@ jobs: name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -93,7 +93,7 @@ jobs: - name: Cache libavif if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libavif with: path: ~/cache-libavif @@ -101,7 +101,7 @@ jobs: - name: Cache libimagequant if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libimagequant with: path: ~/cache-libimagequant @@ -109,7 +109,7 @@ jobs: - name: Cache libwebp if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libwebp with: path: ~/cache-libwebp @@ -162,7 +162,7 @@ jobs: mkdir -p Tests/errors - name: Upload errors - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() with: name: errors @@ -173,7 +173,7 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b5edfc461..80080e2c8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -109,12 +109,12 @@ jobs: os: macos-15-intel cibw_arch: x86_64_iphonesimulator steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false submodules: true - - uses: actions/setup-python@v6 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" @@ -132,7 +132,7 @@ jobs: CIBW_ENABLE: cpython-prerelease pypy MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-${{ matrix.name }} path: ./wheelhouse/*.whl @@ -152,18 +152,18 @@ jobs: - cibw_arch: ARM64 os: windows-11-arm steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Checkout extra test images - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: python-pillow/test-images path: Tests\test-images - - uses: actions/setup-python@v6 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" @@ -212,13 +212,13 @@ jobs: shell: bash - name: Upload wheels - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-windows-${{ matrix.cibw_arch }} path: ./wheelhouse/*.whl - name: Upload fribidi.dll - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: fribidi-windows-${{ matrix.cibw_arch }} path: winbuild\build\bin\fribidi* @@ -227,18 +227,18 @@ jobs: if: github.event_name != 'schedule' || github.event.repository.fork == false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - run: make sdist - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-sdist path: dist/*.tar.gz @@ -248,7 +248,7 @@ jobs: runs-on: ubuntu-latest name: Count dists steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: dist-* path: dist @@ -270,7 +270,7 @@ jobs: name: release-anaconda url: https://anaconda.org/channels/scientific-python-nightly-wheels/packages/pillow/overview steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: dist-!(sdist)* path: dist @@ -292,12 +292,12 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: dist-* path: dist merge-multiple: true - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: attestations: true diff --git a/.github/zizmor.yml b/.github/zizmor.yml deleted file mode 100644 index 100026562..000000000 --- a/.github/zizmor.yml +++ /dev/null @@ -1,6 +0,0 @@ -# https://docs.zizmor.sh/configuration/ -rules: - unpinned-uses: - config: - policies: - "*": ref-pin From 9867b51d894332dcd219f3ea7ac4a6d5b47242fc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 Apr 2026 07:51:50 +1000 Subject: [PATCH 47/83] Catch subprocess.CalledProcessError in test_grab_x11 --- Tests/test_imagegrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 07cb69719..180682c64 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -35,7 +35,7 @@ class TestImageGrab: ImageGrab.grab() ImageGrab.grab(xdisplay="") - except OSError as e: + except (OSError, subprocess.CalledProcessError) as e: pytest.skip(str(e)) @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") From 13433dc0a9c86abb338a01439af3f3795b2e3994 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 21 Apr 2026 11:07:58 -0400 Subject: [PATCH 48/83] 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 2984d8e2b..58d066a1d 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 49/83] 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 58d066a1d..56333f70f 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 50/83] 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 56333f70f..c3e692a9a 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 51/83] 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 c3e692a9a..dad1e4a29 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 52/83] 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 dad1e4a29..34ce3e30f 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 53/83] 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 34ce3e30f..208afb287 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 54/83] 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 208afb287..6046466e8 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 55/83] 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 6046466e8..c13389134 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. From c8c391b9c040ddb0db6b55f1a8da1d58d57156a8 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 15 Apr 2026 13:15:50 -0400 Subject: [PATCH 56/83] Update .github/INCIDENT_RESPONSE.md Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/INCIDENT_RESPONSE.md | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md index 0cefea6cb..1c2e395dd 100644 --- a/.github/INCIDENT_RESPONSE.md +++ b/.github/INCIDENT_RESPONSE.md @@ -97,7 +97,7 @@ Supply-chain and CI/CD incidents are always treated as **Critical** regardless o Vulnerabilities and incidents may be reported or discovered through: 1. **GitHub private security advisory** — preferred channel; see [SECURITY.md](SECURITY.md) -2. **Tidelift security contact** — +2. **Tidelift security contact** — 3. **External researcher / coordinated disclosure** — e.g. Google Project Zero, vendor PSIRT 4. **Automated scanning** — Dependabot, GitHub code-scanning (CodeQL), CI fuzzing 5. **Distro security teams** — Debian, Red Hat, Ubuntu, Alpine may report upstream @@ -230,24 +230,24 @@ require a Pillow point release even if Pillow's own code is unchanged. | Library | Purpose | Security advisory tracker | |---|---|---| | [libjpeg-turbo](https://libjpeg-turbo.org/) | JPEG encode/decode | [GitHub](https://github.com/libjpeg-turbo/libjpeg-turbo/security) | -| [libpng](http://www.libpng.org/pub/png/libpng.html) | PNG encode/decode | [SourceForge](https://sourceforge.net/p/libpng/bugs/) | +| [libpng](http://www.libpng.org/pub/png/libpng.html) | PNG encode/decode within FreeType 2, OpenJPEG and WebP | [SourceForge](https://sourceforge.net/p/libpng/bugs/) | | [libtiff](https://libtiff.gitlab.io/libtiff/) | TIFF encode/decode | [GitLab](https://gitlab.com/libtiff/libtiff/-/work_items) | -| [libwebp](https://chromium.googlesource.com/webm/libwebp) | WebP encode/decode | [Chromium tracker](https://bugs.chromium.org/p/webm/) | +| [libwebp](https://chromium.googlesource.com/webm/libwebp) | WebP encode/decode | [Chromium tracker](https://issues.webmproject.org/issues) | | [libavif](https://github.com/AOMediaCodec/libavif) | AVIF encode/decode | [GitHub](https://github.com/AOMediaCodec/libavif/security) | -| [aom](https://aomedia.googlesource.com/aom/) | AV1 codec (AVIF) | [Chromium tracker](https://bugs.chromium.org/p/aomedia/) | +| [aom](https://aomedia.googlesource.com/aom/) | AV1 codec (AVIF) | [Chromium tracker](https://aomedia.issues.chromium.org/issues) | | [dav1d](https://code.videolan.org/videolan/dav1d) | AV1 decode (AVIF) | [VideoLAN Security](https://www.videolan.org/security/) | | [openjpeg](https://www.openjpeg.org/) | JPEG 2000 encode/decode | [GitHub](https://github.com/uclouvain/openjpeg/security) | | [freetype2](https://freetype.org/) | Font rendering | [GitLab](https://gitlab.freedesktop.org/freetype/freetype/-/work_items) | -| [lcms2](https://www.littlecms.com/) | ICC color management | [GitHub](https://github.com/mm2/Little-CMS) | +| [lcms2](https://www.littlecms.com/) | ICC color management | [GitHub](https://github.com/mm2/Little-CMS/security) | | [harfbuzz](https://harfbuzz.github.io/) | Text shaping (via raqm) | [GitHub](https://github.com/harfbuzz/harfbuzz/security) | | [raqm](https://github.com/HOST-Oman/libraqm) | Complex text layout | [GitHub](https://github.com/HOST-Oman/libraqm) | | [fribidi](https://github.com/fribidi/fribidi) | Unicode bidi (via raqm) | [GitHub](https://github.com/fribidi/fribidi) | | [zlib](https://zlib.net/) | Deflate compression | [zlib.net](https://zlib.net/) | -| [liblzma / xz-utils](https://tukaani.org/xz/) | XZ/LZMA compression | [GitHub](https://github.com/tukaani-project/xz) | +| [liblzma / xz-utils](https://tukaani.org/xz/) | XZ/LZMA compression | [GitHub](https://github.com/tukaani-project/xz/security) | | [bzip2](https://gitlab.com/bzip2/bzip2) | BZ2 compression | [GitLab](https://gitlab.com/bzip2/bzip2/-/work_items) | | [zstd](https://github.com/facebook/zstd) | Zstandard compression | [GitHub](https://github.com/facebook/zstd/security) | -| [brotli](https://github.com/google/brotli) | Brotli compression | [GitHub](https://github.com/google/brotli) | -| [libyuv](https://chromium.googlesource.com/libyuv/libyuv/) | YUV conversion | [Chromium tracker](https://bugs.chromium.org/p/libyuv/) | +| [brotli](https://github.com/google/brotli) | Brotli compression | [GitHub](https://github.com/google/brotli/security) | +| [libyuv](https://chromium.googlesource.com/libyuv/libyuv/) | YUV conversion | [Chromium tracker](https://libyuv.issues.chromium.org/issues) | #### Python-level dependencies @@ -285,7 +285,7 @@ these downstream consumers when assessing severity and planning communications. | Fedora / RHEL / CentOS | `python3-pillow` | [Red Hat Security](https://access.redhat.com/security/) | | Alpine Linux | `py3-pillow` | [Alpine security](https://security.alpinelinux.org/) | | Arch Linux | `python-pillow` | [Arch security tracker](https://security.archlinux.org/) | -| Homebrew | `pillow` | [Homebrew maintainers](https://github.com/Homebrew/homebrew-core) | +| Homebrew | `pillow` | [Homebrew maintainers](https://github.com/Homebrew/homebrew-core/security) | | conda-forge | `pillow` | [conda-forge](https://github.com/conda-forge/pillow-feedstock) | #### Major Python ecosystem consumers @@ -326,7 +326,7 @@ This document is a living record. It should be kept current so it is useful when - [Security Policy](SECURITY.md) - [Release Checklist](../RELEASING.md) - [Contributing Guide](CONTRIBUTING.md) -- [Tidelift Security Contact](https://tidelift.com/security) +- [Tidelift Security Contact](https://tidelift.com/docs/security) - [GitHub: Privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability) - [GitHub as a CVE Numbering Authority (CNA)](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/about-repository-security-advisories) - [FIRST CVSS 4.0 Calculator](https://www.first.org/cvss/calculator/4.0) @@ -369,10 +369,15 @@ This document is a living record. It should be kept current so it is useful when > information confidential until the disclosure date listed below. > > **CVE:** \ +> > **Affected versions:** \ +> > **Fixed version:** \ +> > **Severity:** \ (CVSS \: \) +> > **Reporter:** \ +> > **Public disclosure date:** \ > > **Summary:** @@ -396,9 +401,13 @@ This document is a living record. It should be kept current so it is useful when > **Summary:** \ > > **CVE:** \ +> > **Affected versions:** Pillow \< \ +> > **Fixed version:** \ +> > **Severity:** \ (CVSS \) +> > **Reporter:** \ > > **Details:** From 9605fccf00b19319a90caf35216145cd40084cf4 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 22 Apr 2026 21:25:52 -0400 Subject: [PATCH 57/83] Revise development support information in README Updated development support section with new sponsors. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c6d09a821..4fb31ca93 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,10 @@ Pillow is the friendly PIL fork by [Jeffrey 'Alex' Clark and contributors](https://github.com/python-pillow/Pillow/graphs/contributors). PIL is the Python Imaging Library by Fredrik Lundh and contributors. -As of 2019, Pillow development is -[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). +Development is supported by: +- [Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise) (since 2019) +- [Thanks.dev](https://thanks.dev) (since 2023) +- [GitHub Sponsors](https://github.com/sponsors/python-pillow) (since 2026) From 5f9112e86209c36a4b63d81e5ab6878d9a708790 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 22 Apr 2026 22:22:33 -0400 Subject: [PATCH 58/83] Update README.md Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fb31ca93..b4d83c2a9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Pillow is the friendly PIL fork by [Jeffrey 'Alex' Clark and contributors](https://github.com/python-pillow/Pillow/graphs/contributors). PIL is the Python Imaging Library by Fredrik Lundh and contributors. Development is supported by: -- [Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise) (since 2019) +- [Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise) (since 2018) - [Thanks.dev](https://thanks.dev) (since 2023) - [GitHub Sponsors](https://github.com/sponsors/python-pillow) (since 2026) From a908c624600e46391e9faf0108f50acf16b5bde3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Apr 2026 13:19:01 +1000 Subject: [PATCH 59/83] Skip test_1 for Ghostscript 10.06.0 --- Tests/test_file_eps.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index d4e8db4f4..d41bab307 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,6 +1,7 @@ from __future__ import annotations import io +import subprocess from pathlib import Path import pytest @@ -281,6 +282,11 @@ def test_bytesio_object() -> None: ), ) def test_1(filename: str) -> None: + gs_binary = EpsImagePlugin.gs_binary + assert isinstance(gs_binary, str) + if subprocess.check_output([gs_binary, "--version"]) == b"10.06.0\n": + pytest.skip("Fails with Ghostscript 10.06.0") + with Image.open(filename) as im: assert_image_equal_tofile(im, "Tests/images/eps/1.bmp") From 855774a175d42019c33f319a2bf849489751be19 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:22:45 +0300 Subject: [PATCH 60/83] Test Ubuntu 26.04 Co-authored-by: Andrew Murray --- .github/workflows/test-docker.yml | 10 ++++++++++ docs/installation/building-from-source.rst | 2 +- docs/installation/platform-support.rst | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 083cb9fc2..3210cca33 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -41,6 +41,8 @@ jobs: # Run slower jobs first to give them a headstart and reduce waiting time ubuntu-24.04-noble-ppc64le, ubuntu-24.04-noble-s390x, + ubuntu-26.04-resolute-ppc64le, + ubuntu-26.04-resolute-s390x, # Then run the remainder alpine, amazon-2023-amd64, @@ -53,6 +55,7 @@ jobs: gentoo, ubuntu-22.04-jammy-amd64, ubuntu-24.04-noble-amd64, + ubuntu-26.04-resolute-amd64, ] dockerTag: [main] include: @@ -63,6 +66,13 @@ jobs: - docker: "ubuntu-24.04-noble-arm64v8" os: "ubuntu-24.04-arm" dockerTag: main + - docker: "ubuntu-26.04-resolute-ppc64le" + qemu-arch: "ppc64le" + - docker: "ubuntu-26.04-resolute-s390x" + qemu-arch: "s390x" + - docker: "ubuntu-26.04-resolute-arm64v8" + os: "ubuntu-24.04-arm" + dockerTag: main name: ${{ matrix.docker }} diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 1655b8f60..79d54145a 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -116,7 +116,7 @@ Many of Pillow's features require external libraries: .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. - Prerequisites for **Ubuntu 16.04 LTS - 24.04 LTS** are installed with:: + Prerequisites for **Ubuntu 16.04 LTS - 26.04 LTS** are installed with:: sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 0d6bc2777..2d3735e7a 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -46,6 +46,9 @@ These platforms are built and tested for every change. | | 3.12 | arm64v8, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 26.04 LTS (Resolute)| 3.14 | x86-64, arm64v8, | +| | | ppc64le, s390x | ++----------------------------------+----------------------------+---------------------+ | Windows Server 2022 | 3.10 | x86 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 | From 8c522096e801de47159fc2eaa0e3f9f26d448011 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:08:01 +0300 Subject: [PATCH 61/83] Archive non-amd64 variants of 24.04 --- .github/workflows/test-docker.yml | 9 --------- docs/installation/platform-support.rst | 3 --- 2 files changed, 12 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 3210cca33..82b3c8a23 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -39,8 +39,6 @@ jobs: os: ["ubuntu-latest"] docker: [ # Run slower jobs first to give them a headstart and reduce waiting time - ubuntu-24.04-noble-ppc64le, - ubuntu-24.04-noble-s390x, ubuntu-26.04-resolute-ppc64le, ubuntu-26.04-resolute-s390x, # Then run the remainder @@ -59,13 +57,6 @@ jobs: ] dockerTag: [main] include: - - docker: "ubuntu-24.04-noble-ppc64le" - qemu-arch: "ppc64le" - - docker: "ubuntu-24.04-noble-s390x" - qemu-arch: "s390x" - - docker: "ubuntu-24.04-noble-arm64v8" - os: "ubuntu-24.04-arm" - dockerTag: main - docker: "ubuntu-26.04-resolute-ppc64le" qemu-arch: "ppc64le" - docker: "ubuntu-26.04-resolute-s390x" diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 2d3735e7a..e90d989a2 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -42,9 +42,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 | | | 3.14, 3.15, PyPy3 | | -| +----------------------------+---------------------+ -| | 3.12 | arm64v8, ppc64le, | -| | | s390x | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 26.04 LTS (Resolute)| 3.14 | x86-64, arm64v8, | | | | ppc64le, s390x | From d2b20102e4e695d088835a9f59ca95118c18a80b Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Sat, 25 Apr 2026 17:35:21 -0400 Subject: [PATCH 62/83] Generate CycloneDX SBOM at release time via CI (#9550) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> Co-authored-by: Andrew Murray Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jan Kowalleck --- .ci/requirements-sbom.txt | 1 + .github/generate-sbom.py | 547 +++++++++++++++++++++++++++++++++++ .github/workflows/wheels.yml | 51 ++++ 3 files changed, 599 insertions(+) create mode 100644 .ci/requirements-sbom.txt create mode 100755 .github/generate-sbom.py diff --git a/.ci/requirements-sbom.txt b/.ci/requirements-sbom.txt new file mode 100644 index 000000000..762812f80 --- /dev/null +++ b/.ci/requirements-sbom.txt @@ -0,0 +1 @@ +check-jsonschema==0.37.1 diff --git a/.github/generate-sbom.py b/.github/generate-sbom.py new file mode 100755 index 000000000..9e65121e6 --- /dev/null +++ b/.github/generate-sbom.py @@ -0,0 +1,547 @@ +#!/usr/bin/env python3 +"""Generate a CycloneDX 1.7 SBOM for Pillow's C extensions and their +vendored/optional native library dependencies. + +Usage: + python3 .github/generate-sbom.py [output-file] + +Output defaults to pillow-{version}.cdx.json in the current directory. +""" + +from __future__ import annotations + +import argparse +import base64 +import datetime as dt +import difflib +import hashlib +import json +import urllib.request +import uuid +from pathlib import Path + + +def get_version() -> str: + version_file = Path(__file__).parent.parent / "src" / "PIL" / "_version.py" + return version_file.read_text(encoding="utf-8").split('"')[1] + + +def sha256_file(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def upstream_diff_b64( + upstream_url: str, + upstream_display: bytes, + local_path: Path, + local_display: bytes, +) -> str: + """ + Fetch an upstream file and return a base64-encoded unified diff vs the local copy. + """ + with urllib.request.urlopen(upstream_url) as resp: + upstream_text = resp.read() + local_text = local_path.read_bytes() + diff_lines = difflib.diff_bytes( + difflib.unified_diff, + upstream_text.splitlines(keepends=True), + local_text.splitlines(keepends=True), + fromfile=b"a/" + upstream_display, + tofile=b"b/" + local_display, + ) + return base64.b64encode(b"".join(diff_lines)).decode() + + +def generate(version: str) -> dict: + serial = str(uuid.uuid4()) + now = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + purl = f"pkg:pypi/pillow@{version}" + root = Path(__file__).parent.parent + thirdparty = root / "src" / "thirdparty" + + metadata_component = { + "bom-ref": purl, + "type": "library", + "name": "Pillow", + "version": version, + "description": "Python Imaging Library (fork)", + "licenses": [{"license": {"id": "MIT-CMU"}}], + "purl": purl, + "externalReferences": [ + {"type": "website", "url": "https://python-pillow.github.io"}, + {"type": "vcs", "url": "https://github.com/python-pillow/Pillow"}, + {"type": "documentation", "url": "https://pillow.readthedocs.io"}, + { + "type": "security-contact", + "url": "https://github.com/python-pillow/Pillow/security/policy", + }, + ], + } + + c_extensions = [ + ( + "PIL._imaging", + "Core image processing extension " + "(decode, encode, map, display, outline, path, libImaging)", + ), + ("PIL._imagingft", "FreeType font rendering extension"), + ("PIL._imagingcms", "LittleCMS2 colour management extension"), + ("PIL._webp", "WebP image format extension"), + ("PIL._avif", "AVIF image format extension"), + ("PIL._imagingtk", "Tk/Tcl display extension"), + ("PIL._imagingmath", "Image math operations extension"), + ("PIL._imagingmorph", "Image morphology extension"), + ] + + ext_components = [ + { + "bom-ref": f"{purl}#c-ext/{name}", + "type": "library", + "name": name, + "version": version, + "description": desc, + "licenses": [{"license": {"id": "MIT-CMU"}}], + "purl": f"{purl}#c-ext/{name}", + } + for name, desc in c_extensions + ] + + vendored_components = [ + { + "bom-ref": f"{purl}#thirdparty/raqm", + "type": "library", + "name": "raqm", + "version": "0.10.5", + "description": "Complex text layout library " + "(vendored in src/thirdparty/raqm/)", + "licenses": [{"license": {"id": "MIT"}}], + "hashes": [ + { + "alg": "SHA-256", + "content": sha256_file(thirdparty / "raqm" / "raqm.c"), + } + ], + "pedigree": { + "ancestors": [ + { + "bom-ref": "pkg:github/HOST-Oman/libraqm@0.10.5#upstream", + "type": "library", + "name": "raqm", + "version": "0.10.5", + "purl": "pkg:github/HOST-Oman/libraqm@0.10.5", + "externalReferences": [ + { + "type": "distribution", + "url": "https://github.com/HOST-Oman/libraqm/releases/tag/v0.10.5", + } + ], + } + ], + "patches": [ + { + "type": "unofficial", + "diff": { + "text": { + # raqm-version.h.in → raqm-version.h: + # template @RAQM_VERSION_*@ placeholders replaced + # with literal 0.10.5 values; filename changed to + # drop the .in suffix; minor indentation fix. + "content": upstream_diff_b64( + "https://raw.githubusercontent.com/HOST-Oman/libraqm/v0.10.5/src/raqm-version.h.in", + b"src/raqm-version.h.in", + thirdparty / "raqm" / "raqm-version.h", + b"src/raqm-version.h", + ), + "encoding": "base64", + } + }, + }, + { + "type": "unofficial", + "diff": { + "text": { + # raqm.c: wrap the include in an + # #ifdef HAVE_FRIBIDI_SYSTEM guard so that when + # building without a system FriBiDi Pillow's own + # fribidi-shim is used instead. + "content": upstream_diff_b64( + "https://raw.githubusercontent.com/HOST-Oman/libraqm/v0.10.5/src/raqm.c", + b"src/raqm.c", + thirdparty / "raqm" / "raqm.c", + b"src/raqm.c", + ), + "encoding": "base64", + } + }, + }, + ], + "notes": ( + "Vendored from upstream HOST-Oman/libraqm v0.10.5 with two " + "Pillow-specific modifications: (1) raqm-version.h.in was " + "pre-processed into raqm-version.h with version placeholders " + "replaced by literal values; (2) raqm.c wraps the " + "include in an #ifdef HAVE_FRIBIDI_SYSTEM guard so Pillow's " + "bundled fribidi-shim is used when a system FriBiDi is absent." + ), + }, + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/python-pillow/Pillow/tree/main/src/thirdparty/raqm", + }, + ], + }, + { + "bom-ref": f"{purl}#thirdparty/fribidi-shim", + "type": "library", + "name": "fribidi-shim", + "version": "1.x", + "description": "FriBiDi runtime-loading shim " + "(vendored in src/thirdparty/fribidi-shim/); " + "loads libfribidi dynamically", + "licenses": [{"license": {"id": "LGPL-2.1-or-later"}}], + "hashes": [ + { + "alg": "SHA-256", + "content": sha256_file(thirdparty / "fribidi-shim" / "fribidi.c"), + } + ], + "pedigree": { + "notes": "Pillow-authored shim; not taken from an upstream project." + }, + "externalReferences": [ + {"type": "website", "url": "https://github.com/fribidi/fribidi"}, + ], + }, + { + "bom-ref": "pkg:github/python/pythoncapi-compat", + "type": "library", + "name": "pythoncapi_compat", + "description": "Backport header for new CPython C-API functions " + "(vendored in src/thirdparty/pythoncapi_compat.h)", + "licenses": [{"license": {"id": "0BSD"}}], + "hashes": [ + { + "alg": "SHA-256", + "content": sha256_file(thirdparty / "pythoncapi_compat.h"), + } + ], + "pedigree": { + "notes": "Vendored unmodified from upstream python/pythoncapi-compat." + }, + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/python/pythoncapi-compat", + }, + ], + }, + ] + + native_deps = [ + { + "bom-ref": "pkg:generic/libjpeg", + "type": "library", + "name": "libjpeg / libjpeg-turbo", + "description": "JPEG codec (required by default; disable with " + "-C jpeg=disable). Tested with libjpeg 6b/8/9-9d " + "and libjpeg-turbo 2-3.", + "licenses": [ + {"license": {"id": "IJG"}}, + {"license": {"id": "BSD-3-Clause"}}, + ], + "externalReferences": [ + {"type": "website", "url": "https://ijg.org"}, + {"type": "website", "url": "https://libjpeg-turbo.org"}, + { + "type": "distribution", + "url": "https://github.com/libjpeg-turbo/libjpeg-turbo/releases", + }, + ], + }, + { + "bom-ref": "pkg:generic/zlib", + "type": "library", + "name": "zlib", + "description": "Deflate/PNG compression (required by default; " + "disable with -C zlib=disable).", + "licenses": [{"license": {"id": "Zlib"}}], + "externalReferences": [ + {"type": "website", "url": "https://zlib.net"}, + {"type": "distribution", "url": "https://zlib.net"}, + ], + }, + { + "bom-ref": "pkg:generic/libtiff", + "type": "library", + "name": "libtiff", + "scope": "optional", + "description": "TIFF codec (optional). Tested with libtiff 4.0-4.7.1.", + "licenses": [{"license": {"id": "libtiff"}}], + "externalReferences": [ + {"type": "website", "url": "https://libtiff.gitlab.io/libtiff/"}, + { + "type": "distribution", + "url": "https://download.osgeo.org/libtiff/", + }, + ], + }, + { + "bom-ref": "pkg:generic/freetype2", + "type": "library", + "name": "FreeType", + "scope": "optional", + "description": "Font rendering (optional, used by PIL._imagingft). " + "Required for text/font support.", + "licenses": [{"license": {"id": "FTL"}}], + "externalReferences": [ + {"type": "website", "url": "https://freetype.org"}, + { + "type": "distribution", + "url": "https://download.savannah.gnu.org/releases/freetype/", + }, + ], + }, + { + "bom-ref": "pkg:generic/littlecms2", + "type": "library", + "name": "Little CMS 2", + "scope": "optional", + "description": "Colour management (optional, used by PIL._imagingcms). " + "Tested with lcms2 2.7-2.18.", + "licenses": [{"license": {"id": "MIT"}}], + "externalReferences": [ + {"type": "website", "url": "https://www.littlecms.com"}, + { + "type": "distribution", + "url": "https://github.com/mm2/Little-CMS/releases", + }, + ], + }, + { + "bom-ref": "pkg:generic/libwebp", + "type": "library", + "name": "libwebp", + "scope": "optional", + "description": "WebP codec (optional, used by PIL._webp).", + "licenses": [{"license": {"id": "BSD-3-Clause"}}], + "externalReferences": [ + { + "type": "website", + "url": "https://chromium.googlesource.com/webm/libwebp", + }, + { + "type": "distribution", + "url": "https://chromium.googlesource.com/webm/libwebp", + }, + ], + }, + { + "bom-ref": "pkg:generic/openjpeg", + "type": "library", + "name": "OpenJPEG", + "scope": "optional", + "description": "JPEG 2000 codec (optional). " + "Tested with openjpeg 2.0.0-2.5.4.", + "licenses": [{"license": {"id": "BSD-2-Clause"}}], + "externalReferences": [ + {"type": "website", "url": "https://www.openjpeg.org"}, + { + "type": "distribution", + "url": "https://github.com/uclouvain/openjpeg/releases", + }, + ], + }, + { + "bom-ref": "pkg:generic/libavif", + "type": "library", + "name": "libavif", + "scope": "optional", + "description": "AVIF codec (optional, used by PIL._avif). " + "Requires libavif >= 1.0.0.", + "licenses": [{"license": {"id": "BSD-2-Clause"}}], + "externalReferences": [ + {"type": "website", "url": "https://github.com/AOMediaCodec/libavif"}, + { + "type": "distribution", + "url": "https://github.com/AOMediaCodec/libavif/releases", + }, + ], + }, + { + "bom-ref": "pkg:generic/harfbuzz", + "type": "library", + "name": "HarfBuzz", + "scope": "optional", + "description": "Text shaping (optional, required by libraqm " + "for complex text layout).", + "licenses": [{"license": {"id": "MIT"}}], + "externalReferences": [ + {"type": "website", "url": "https://harfbuzz.github.io"}, + { + "type": "distribution", + "url": "https://github.com/harfbuzz/harfbuzz/releases", + }, + ], + }, + { + "bom-ref": "pkg:generic/fribidi", + "type": "library", + "name": "FriBiDi", + "scope": "optional", + "description": "Unicode bidi algorithm library (optional, " + "loaded at runtime by fribidi-shim).", + "licenses": [{"license": {"id": "LGPL-2.1-or-later"}}], + "externalReferences": [ + {"type": "website", "url": "https://github.com/fribidi/fribidi"}, + { + "type": "distribution", + "url": "https://github.com/fribidi/fribidi/releases", + }, + ], + }, + { + "bom-ref": "pkg:generic/libimagequant", + "type": "library", + "name": "libimagequant", + "scope": "optional", + "description": "Improved colour quantization (optional). " + "Tested with 2.6-4.4.1.", + "licenses": [{"license": {"id": "GPL-3.0-or-later"}}], + "externalReferences": [ + {"type": "website", "url": "https://pngquant.org/lib/"}, + { + "type": "distribution", + "url": "https://github.com/ImageOptim/libimagequant/tags", + }, + ], + }, + { + "bom-ref": "pkg:generic/libxcb", + "type": "library", + "name": "libxcb", + "scope": "optional", + "description": "X11 screen-grab support (optional, " + "used by PIL._imaging on macOS and Linux).", + "licenses": [{"license": {"id": "X11"}}], + "externalReferences": [ + {"type": "website", "url": "https://xcb.freedesktop.org"}, + { + "type": "distribution", + "url": "https://xcb.freedesktop.org/dist/", + }, + ], + }, + { + "bom-ref": "pkg:pypi/pybind11", + "type": "library", + "name": "pybind11", + "scope": "excluded", + "description": "Parallel C compilation library (build-time dependency).", + "licenses": [{"license": {"id": "BSD-3-Clause"}}], + "externalReferences": [ + {"type": "website", "url": "https://pybind11.readthedocs.io"}, + { + "type": "distribution", + "url": "https://github.com/pybind/pybind11/releases", + }, + ], + }, + ] + + dependencies = [ + { + "ref": purl, + "dependsOn": [e["bom-ref"] for e in ext_components], + }, + { + "ref": f"{purl}#c-ext/PIL._imaging", + "dependsOn": [ + "pkg:generic/libjpeg", + "pkg:generic/zlib", + "pkg:generic/libtiff", + "pkg:generic/openjpeg", + "pkg:generic/libimagequant", + "pkg:generic/libxcb", + ], + }, + { + "ref": f"{purl}#c-ext/PIL._imagingft", + "dependsOn": [ + "pkg:generic/freetype2", + f"{purl}#thirdparty/raqm", + f"{purl}#thirdparty/fribidi-shim", + "pkg:generic/harfbuzz", + "pkg:generic/fribidi", + ], + }, + { + "ref": f"{purl}#c-ext/PIL._imagingcms", + "dependsOn": ["pkg:generic/littlecms2"], + }, + { + "ref": f"{purl}#c-ext/PIL._webp", + "dependsOn": ["pkg:generic/libwebp"], + }, + { + "ref": f"{purl}#c-ext/PIL._avif", + "dependsOn": ["pkg:generic/libavif"], + }, + { + "ref": f"{purl}#thirdparty/raqm", + "dependsOn": [ + f"{purl}#thirdparty/fribidi-shim", + "pkg:generic/harfbuzz", + ], + }, + ] + + return { + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7", + "serialNumber": f"urn:uuid:{serial}", + "version": 1, + "metadata": { + "timestamp": now, + "lifecycles": [{"phase": "build"}], + "tools": { + "components": [ + { + "type": "application", + "name": "generate-sbom.py", + "group": "pillow", + } + ] + }, + "component": metadata_component, + }, + "components": ext_components + vendored_components + native_deps, + "dependencies": dependencies, + } + + +def main() -> None: + version = get_version() + + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + "output", + nargs="?", + type=Path, + default=Path(f"pillow-{version}.cdx.json"), + help="output file", + ) + args = parser.parse_args() + + sbom = generate(version) + args.output.write_text(json.dumps(sbom, indent=2) + "\n", encoding="utf-8") + print( + f"Wrote {args.output} (Pillow {version}, {len(sbom['components'])} components)" + ) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 80080e2c8..cc9e69428 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -12,7 +12,9 @@ on: push: paths: - ".ci/requirements-cibw.txt" + - ".ci/requirements-sbom.txt" - ".github/dependencies.json" + - ".github/generate-sbom.py" - ".github/workflows/wheels*" - "pyproject.toml" - "setup.py" @@ -24,7 +26,9 @@ on: pull_request: paths: - ".ci/requirements-cibw.txt" + - ".ci/requirements-sbom.txt" - ".github/dependencies.json" + - ".github/generate-sbom.py" - ".github/workflows/wheels*" - "pyproject.toml" - "setup.py" @@ -281,6 +285,53 @@ jobs: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} + sbom: + runs-on: ubuntu-latest + name: Generate SBOM + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.x" + + - name: Generate CycloneDX SBOM + run: python3 .github/generate-sbom.py + + - name: Upload SBOM as workflow artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sbom + path: "*.cdx.json" + + - name: Validate SBOM + run: | + python3 -m pip install -r .ci/requirements-sbom.txt + check-jsonschema --schemafile "https://raw.githubusercontent.com/CycloneDX/specification/1.7/schema/bom-1.7.schema.json" *.cdx.json + + sbom-publish: + if: | + github.event.repository.fork == false + && github.event_name == 'push' + && startsWith(github.ref, 'refs/tags') + needs: [count-dists, sbom] + runs-on: ubuntu-latest + name: Publish SBOM to GitHub release + permissions: + contents: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: sbom + path: . + + - name: Attach SBOM to GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload "$GITHUB_REF_NAME" *.cdx.json + pypi-publish: if: github.event.repository.fork == false && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') needs: count-dists From f0fe496315c1692f3d914ae908fec516e1c24521 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:44:52 +0300 Subject: [PATCH 63/83] Fix typo to trigger on self change --- .github/workflows/test-valgrind-memory.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index 1cbcc40d3..5ceb544ea 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -8,12 +8,13 @@ on: # branches: # - "**" # paths: - # - ".github/workflows/test-valgrind.yml" + # - ".github/workflows/test-valgrind-memory.yml" # - "**.c" # - "**.h" + # - "depends/docker-test-valgrind-memory.sh" pull_request: paths: - - ".github/workflows/test-valgrind.yml" + - ".github/workflows/test-valgrind-memory.yml" - "**.c" - "**.h" - "depends/docker-test-valgrind-memory.sh" From 755b73b274a9aeb2fb4c171762972a9bfc2beff3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:14:13 +0300 Subject: [PATCH 64/83] Deduplicate path triggers in workflows --- .github/workflows/cifuzz.yml | 9 ++------- .github/workflows/docs.yml | 7 ++----- .github/workflows/test-docker.yml | 9 ++------- .github/workflows/test-mingw.yml | 9 ++------- .github/workflows/test-valgrind.yml | 7 ++----- .github/workflows/test-windows.yml | 9 ++------- .github/workflows/test.yml | 9 ++------- .github/workflows/wheels.yml | 14 ++------------ 8 files changed, 16 insertions(+), 57 deletions(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index cc8b4606b..27b55cffc 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -4,19 +4,14 @@ on: push: branches: - "**" - paths: + paths: &paths - ".github/dependencies.json" - ".github/workflows/cifuzz.yml" - ".github/workflows/wheels-dependencies.sh" - "**.c" - "**.h" pull_request: - paths: - - ".github/dependencies.json" - - ".github/workflows/cifuzz.yml" - - ".github/workflows/wheels-dependencies.sh" - - "**.c" - - "**.h" + paths: *paths workflow_dispatch: permissions: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8c29af7b7..3734a3306 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,15 +4,12 @@ on: push: branches: - "**" - paths: + paths: &paths - ".github/workflows/docs.yml" - "docs/**" - "src/PIL/**" pull_request: - paths: - - ".github/workflows/docs.yml" - - "docs/**" - - "src/PIL/**" + paths: *paths workflow_dispatch: permissions: diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 82b3c8a23..b035ac1de 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -4,19 +4,14 @@ on: push: branches: - "**" - paths-ignore: + paths-ignore: &paths-ignore - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - "docs/**" - "wheels/**" pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" + paths-ignore: *paths-ignore workflow_dispatch: permissions: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a87928f0b..1c36e06c0 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -4,19 +4,14 @@ on: push: branches: - "**" - paths-ignore: + paths-ignore: &paths-ignore - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - "docs/**" - "wheels/**" pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" + paths-ignore: *paths-ignore workflow_dispatch: permissions: diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index f3ec8c10e..c47a0d060 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -6,15 +6,12 @@ on: push: branches: - "**" - paths: + paths: &paths - ".github/workflows/test-valgrind.yml" - "**.c" - "**.h" pull_request: - paths: - - ".github/workflows/test-valgrind.yml" - - "**.c" - - "**.h" + paths: *paths workflow_dispatch: permissions: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6a83338d8..fa1898df2 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -4,19 +4,14 @@ on: push: branches: - "**" - paths-ignore: + paths-ignore: &paths-ignore - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - "docs/**" - "wheels/**" pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" + paths-ignore: *paths-ignore workflow_dispatch: permissions: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2654e2d04..d90cc805a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,19 +4,14 @@ on: push: branches: - "**" - paths-ignore: + paths-ignore: &paths-ignore - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - "docs/**" - "wheels/**" pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" + paths-ignore: *paths-ignore workflow_dispatch: permissions: diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index cc9e69428..98733b6c7 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -10,7 +10,7 @@ on: # │ │ │ │ │ - cron: "42 1 * * 0,3" push: - paths: + paths: &paths - ".ci/requirements-cibw.txt" - ".ci/requirements-sbom.txt" - ".github/dependencies.json" @@ -24,17 +24,7 @@ on: tags: - "*" pull_request: - paths: - - ".ci/requirements-cibw.txt" - - ".ci/requirements-sbom.txt" - - ".github/dependencies.json" - - ".github/generate-sbom.py" - - ".github/workflows/wheels*" - - "pyproject.toml" - - "setup.py" - - "wheels/*" - - "winbuild/build_prepare.py" - - "winbuild/fribidi.cmake" + paths: *paths workflow_dispatch: permissions: From fe054a1b3f29a5de2be3788cf91231915cf61c87 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 28 Apr 2026 08:53:21 +1000 Subject: [PATCH 65/83] Added CVEs to 12.2.0 release notes (#9591) Co-authored-by: Andrew Murray --- docs/releasenotes/12.2.0.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst index 0fee9fd82..da678a47b 100644 --- a/docs/releasenotes/12.2.0.rst +++ b/docs/releasenotes/12.2.0.rst @@ -13,28 +13,28 @@ introduced in Pillow 10.3.0. The data being read is now limited to only the necessary amount. -Fix OOB write with invalid tile extents -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2026-42311`: Fix OOB write with invalid tile extents +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow 12.1.1 addressed :cve:`2026-25990` by improving checks for tile extents to prevent an OOB write from specially crafted PSD images in Pillow >= 10.3.0. However, these checks did not consider integer overflow. This has been corrected. -Prevent PDF parsing trailer infinite loop -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2026-42310`: Prevent PDF parsing trailer infinite loop +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When parsing a PDF, if a trailer refers to itself, or a more complex cyclic loop exists, then an infinite loop occurs. Pillow now keeps a record of which trailers it has already processed. PdfParser was added in Pillow 4.2.0. -Integer overflow when processing fonts -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2026-42308`: Integer overflow when processing fonts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If a font advances for each glyph by an exceeding large amount, when Pillow keeps track of the current position, it may lead to an integer overflow. This has been fixed. -Heap buffer overflow with nested list coordinates -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2026-42309`: Heap buffer overflow with nested list coordinates +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Passing nested lists as coordinates to APIs that accept coordinates such as ``ImagePath.Path``, :py:meth:`~PIL.ImageDraw.ImageDraw.polygon` From 99869f031342ad718721b66a791bf0b6eba2fb5e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:14:58 +0300 Subject: [PATCH 66/83] Sort things alphabetically to make easier to find --- .github/generate-sbom.py | 386 +++++++++++++++++++-------------------- 1 file changed, 193 insertions(+), 193 deletions(-) diff --git a/.github/generate-sbom.py b/.github/generate-sbom.py index 9e65121e6..fb9b37f27 100755 --- a/.github/generate-sbom.py +++ b/.github/generate-sbom.py @@ -79,18 +79,18 @@ def generate(version: str) -> dict: } c_extensions = [ + ("PIL._avif", "AVIF image format extension"), ( "PIL._imaging", "Core image processing extension " "(decode, encode, map, display, outline, path, libImaging)", ), - ("PIL._imagingft", "FreeType font rendering extension"), ("PIL._imagingcms", "LittleCMS2 colour management extension"), - ("PIL._webp", "WebP image format extension"), - ("PIL._avif", "AVIF image format extension"), - ("PIL._imagingtk", "Tk/Tcl display extension"), + ("PIL._imagingft", "FreeType font rendering extension"), ("PIL._imagingmath", "Image math operations extension"), ("PIL._imagingmorph", "Image morphology extension"), + ("PIL._imagingtk", "Tk/Tcl display extension"), + ("PIL._webp", "WebP image format extension"), ] ext_components = [ @@ -107,6 +107,51 @@ def generate(version: str) -> dict: ] vendored_components = [ + { + "bom-ref": f"{purl}#thirdparty/fribidi-shim", + "type": "library", + "name": "fribidi-shim", + "version": "1.x", + "description": "FriBiDi runtime-loading shim " + "(vendored in src/thirdparty/fribidi-shim/); " + "loads libfribidi dynamically", + "licenses": [{"license": {"id": "LGPL-2.1-or-later"}}], + "hashes": [ + { + "alg": "SHA-256", + "content": sha256_file(thirdparty / "fribidi-shim" / "fribidi.c"), + } + ], + "pedigree": { + "notes": "Pillow-authored shim; not taken from an upstream project." + }, + "externalReferences": [ + {"type": "website", "url": "https://github.com/fribidi/fribidi"}, + ], + }, + { + "bom-ref": "pkg:github/python/pythoncapi-compat", + "type": "library", + "name": "pythoncapi_compat", + "description": "Backport header for new CPython C-API functions " + "(vendored in src/thirdparty/pythoncapi_compat.h)", + "licenses": [{"license": {"id": "0BSD"}}], + "hashes": [ + { + "alg": "SHA-256", + "content": sha256_file(thirdparty / "pythoncapi_compat.h"), + } + ], + "pedigree": { + "notes": "Vendored unmodified from upstream python/pythoncapi-compat." + }, + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/python/pythoncapi-compat", + }, + ], + }, { "bom-ref": f"{purl}#thirdparty/raqm", "type": "library", @@ -191,54 +236,89 @@ def generate(version: str) -> dict: }, ], }, - { - "bom-ref": f"{purl}#thirdparty/fribidi-shim", - "type": "library", - "name": "fribidi-shim", - "version": "1.x", - "description": "FriBiDi runtime-loading shim " - "(vendored in src/thirdparty/fribidi-shim/); " - "loads libfribidi dynamically", - "licenses": [{"license": {"id": "LGPL-2.1-or-later"}}], - "hashes": [ - { - "alg": "SHA-256", - "content": sha256_file(thirdparty / "fribidi-shim" / "fribidi.c"), - } - ], - "pedigree": { - "notes": "Pillow-authored shim; not taken from an upstream project." - }, - "externalReferences": [ - {"type": "website", "url": "https://github.com/fribidi/fribidi"}, - ], - }, - { - "bom-ref": "pkg:github/python/pythoncapi-compat", - "type": "library", - "name": "pythoncapi_compat", - "description": "Backport header for new CPython C-API functions " - "(vendored in src/thirdparty/pythoncapi_compat.h)", - "licenses": [{"license": {"id": "0BSD"}}], - "hashes": [ - { - "alg": "SHA-256", - "content": sha256_file(thirdparty / "pythoncapi_compat.h"), - } - ], - "pedigree": { - "notes": "Vendored unmodified from upstream python/pythoncapi-compat." - }, - "externalReferences": [ - { - "type": "vcs", - "url": "https://github.com/python/pythoncapi-compat", - }, - ], - }, ] native_deps = [ + { + "bom-ref": "pkg:generic/freetype2", + "type": "library", + "name": "FreeType", + "scope": "optional", + "description": "Font rendering (optional, used by PIL._imagingft). " + "Required for text/font support.", + "licenses": [{"license": {"id": "FTL"}}], + "externalReferences": [ + {"type": "website", "url": "https://freetype.org"}, + { + "type": "distribution", + "url": "https://download.savannah.gnu.org/releases/freetype/", + }, + ], + }, + { + "bom-ref": "pkg:generic/fribidi", + "type": "library", + "name": "FriBiDi", + "scope": "optional", + "description": "Unicode bidi algorithm library (optional, " + "loaded at runtime by fribidi-shim).", + "licenses": [{"license": {"id": "LGPL-2.1-or-later"}}], + "externalReferences": [ + {"type": "website", "url": "https://github.com/fribidi/fribidi"}, + { + "type": "distribution", + "url": "https://github.com/fribidi/fribidi/releases", + }, + ], + }, + { + "bom-ref": "pkg:generic/harfbuzz", + "type": "library", + "name": "HarfBuzz", + "scope": "optional", + "description": "Text shaping (optional, required by libraqm " + "for complex text layout).", + "licenses": [{"license": {"id": "MIT"}}], + "externalReferences": [ + {"type": "website", "url": "https://harfbuzz.github.io"}, + { + "type": "distribution", + "url": "https://github.com/harfbuzz/harfbuzz/releases", + }, + ], + }, + { + "bom-ref": "pkg:generic/libavif", + "type": "library", + "name": "libavif", + "scope": "optional", + "description": "AVIF codec (optional, used by PIL._avif). " + "Requires libavif >= 1.0.0.", + "licenses": [{"license": {"id": "BSD-2-Clause"}}], + "externalReferences": [ + {"type": "website", "url": "https://github.com/AOMediaCodec/libavif"}, + { + "type": "distribution", + "url": "https://github.com/AOMediaCodec/libavif/releases", + }, + ], + }, + { + "bom-ref": "pkg:generic/libimagequant", + "type": "library", + "name": "libimagequant", + "scope": "optional", + "description": "Improved colour quantization (optional). " + "Tested with 2.6-4.4.1.", + "licenses": [{"license": {"id": "GPL-3.0-or-later"}}], + "externalReferences": [ + {"type": "website", "url": "https://pngquant.org/lib/"}, + { + "type": "distribution", + "url": "https://github.com/ImageOptim/libimagequant/tags", + }, + ], + }, { "bom-ref": "pkg:generic/libjpeg", "type": "library", @@ -259,18 +339,6 @@ def generate(version: str) -> dict: }, ], }, - { - "bom-ref": "pkg:generic/zlib", - "type": "library", - "name": "zlib", - "description": "Deflate/PNG compression (required by default; " - "disable with -C zlib=disable).", - "licenses": [{"license": {"id": "Zlib"}}], - "externalReferences": [ - {"type": "website", "url": "https://zlib.net"}, - {"type": "distribution", "url": "https://zlib.net"}, - ], - }, { "bom-ref": "pkg:generic/libtiff", "type": "library", @@ -286,38 +354,6 @@ def generate(version: str) -> dict: }, ], }, - { - "bom-ref": "pkg:generic/freetype2", - "type": "library", - "name": "FreeType", - "scope": "optional", - "description": "Font rendering (optional, used by PIL._imagingft). " - "Required for text/font support.", - "licenses": [{"license": {"id": "FTL"}}], - "externalReferences": [ - {"type": "website", "url": "https://freetype.org"}, - { - "type": "distribution", - "url": "https://download.savannah.gnu.org/releases/freetype/", - }, - ], - }, - { - "bom-ref": "pkg:generic/littlecms2", - "type": "library", - "name": "Little CMS 2", - "scope": "optional", - "description": "Colour management (optional, used by PIL._imagingcms). " - "Tested with lcms2 2.7-2.18.", - "licenses": [{"license": {"id": "MIT"}}], - "externalReferences": [ - {"type": "website", "url": "https://www.littlecms.com"}, - { - "type": "distribution", - "url": "https://github.com/mm2/Little-CMS/releases", - }, - ], - }, { "bom-ref": "pkg:generic/libwebp", "type": "library", @@ -336,86 +372,6 @@ def generate(version: str) -> dict: }, ], }, - { - "bom-ref": "pkg:generic/openjpeg", - "type": "library", - "name": "OpenJPEG", - "scope": "optional", - "description": "JPEG 2000 codec (optional). " - "Tested with openjpeg 2.0.0-2.5.4.", - "licenses": [{"license": {"id": "BSD-2-Clause"}}], - "externalReferences": [ - {"type": "website", "url": "https://www.openjpeg.org"}, - { - "type": "distribution", - "url": "https://github.com/uclouvain/openjpeg/releases", - }, - ], - }, - { - "bom-ref": "pkg:generic/libavif", - "type": "library", - "name": "libavif", - "scope": "optional", - "description": "AVIF codec (optional, used by PIL._avif). " - "Requires libavif >= 1.0.0.", - "licenses": [{"license": {"id": "BSD-2-Clause"}}], - "externalReferences": [ - {"type": "website", "url": "https://github.com/AOMediaCodec/libavif"}, - { - "type": "distribution", - "url": "https://github.com/AOMediaCodec/libavif/releases", - }, - ], - }, - { - "bom-ref": "pkg:generic/harfbuzz", - "type": "library", - "name": "HarfBuzz", - "scope": "optional", - "description": "Text shaping (optional, required by libraqm " - "for complex text layout).", - "licenses": [{"license": {"id": "MIT"}}], - "externalReferences": [ - {"type": "website", "url": "https://harfbuzz.github.io"}, - { - "type": "distribution", - "url": "https://github.com/harfbuzz/harfbuzz/releases", - }, - ], - }, - { - "bom-ref": "pkg:generic/fribidi", - "type": "library", - "name": "FriBiDi", - "scope": "optional", - "description": "Unicode bidi algorithm library (optional, " - "loaded at runtime by fribidi-shim).", - "licenses": [{"license": {"id": "LGPL-2.1-or-later"}}], - "externalReferences": [ - {"type": "website", "url": "https://github.com/fribidi/fribidi"}, - { - "type": "distribution", - "url": "https://github.com/fribidi/fribidi/releases", - }, - ], - }, - { - "bom-ref": "pkg:generic/libimagequant", - "type": "library", - "name": "libimagequant", - "scope": "optional", - "description": "Improved colour quantization (optional). " - "Tested with 2.6-4.4.1.", - "licenses": [{"license": {"id": "GPL-3.0-or-later"}}], - "externalReferences": [ - {"type": "website", "url": "https://pngquant.org/lib/"}, - { - "type": "distribution", - "url": "https://github.com/ImageOptim/libimagequant/tags", - }, - ], - }, { "bom-ref": "pkg:generic/libxcb", "type": "library", @@ -432,6 +388,38 @@ def generate(version: str) -> dict: }, ], }, + { + "bom-ref": "pkg:generic/littlecms2", + "type": "library", + "name": "Little CMS 2", + "scope": "optional", + "description": "Colour management (optional, used by PIL._imagingcms). " + "Tested with lcms2 2.7-2.18.", + "licenses": [{"license": {"id": "MIT"}}], + "externalReferences": [ + {"type": "website", "url": "https://www.littlecms.com"}, + { + "type": "distribution", + "url": "https://github.com/mm2/Little-CMS/releases", + }, + ], + }, + { + "bom-ref": "pkg:generic/openjpeg", + "type": "library", + "name": "OpenJPEG", + "scope": "optional", + "description": "JPEG 2000 codec (optional). " + "Tested with openjpeg 2.0.0-2.5.4.", + "licenses": [{"license": {"id": "BSD-2-Clause"}}], + "externalReferences": [ + {"type": "website", "url": "https://www.openjpeg.org"}, + { + "type": "distribution", + "url": "https://github.com/uclouvain/openjpeg/releases", + }, + ], + }, { "bom-ref": "pkg:pypi/pybind11", "type": "library", @@ -447,51 +435,63 @@ def generate(version: str) -> dict: }, ], }, + { + "bom-ref": "pkg:generic/zlib", + "type": "library", + "name": "zlib", + "description": "Deflate/PNG compression (required by default; " + "disable with -C zlib=disable).", + "licenses": [{"license": {"id": "Zlib"}}], + "externalReferences": [ + {"type": "website", "url": "https://zlib.net"}, + {"type": "distribution", "url": "https://zlib.net"}, + ], + }, ] dependencies = [ { "ref": purl, - "dependsOn": [e["bom-ref"] for e in ext_components], + "dependsOn": sorted(e["bom-ref"] for e in ext_components), + }, + { + "ref": f"{purl}#c-ext/PIL._avif", + "dependsOn": ["pkg:generic/libavif"], }, { "ref": f"{purl}#c-ext/PIL._imaging", "dependsOn": [ - "pkg:generic/libjpeg", - "pkg:generic/zlib", - "pkg:generic/libtiff", - "pkg:generic/openjpeg", "pkg:generic/libimagequant", + "pkg:generic/libjpeg", + "pkg:generic/libtiff", "pkg:generic/libxcb", - ], - }, - { - "ref": f"{purl}#c-ext/PIL._imagingft", - "dependsOn": [ - "pkg:generic/freetype2", - f"{purl}#thirdparty/raqm", - f"{purl}#thirdparty/fribidi-shim", - "pkg:generic/harfbuzz", - "pkg:generic/fribidi", + "pkg:generic/openjpeg", + "pkg:generic/zlib", ], }, { "ref": f"{purl}#c-ext/PIL._imagingcms", "dependsOn": ["pkg:generic/littlecms2"], }, + { + "ref": f"{purl}#c-ext/PIL._imagingft", + "dependsOn": [ + "pkg:generic/freetype2", + "pkg:generic/fribidi", + "pkg:generic/harfbuzz", + f"{purl}#thirdparty/fribidi-shim", + f"{purl}#thirdparty/raqm", + ], + }, { "ref": f"{purl}#c-ext/PIL._webp", "dependsOn": ["pkg:generic/libwebp"], }, - { - "ref": f"{purl}#c-ext/PIL._avif", - "dependsOn": ["pkg:generic/libavif"], - }, { "ref": f"{purl}#thirdparty/raqm", "dependsOn": [ - f"{purl}#thirdparty/fribidi-shim", "pkg:generic/harfbuzz", + f"{purl}#thirdparty/fribidi-shim", ], }, ] From f2ee74b2f8756f7ff162d65b0e501c635a169e24 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:19:01 +0300 Subject: [PATCH 67/83] Use versions from dependencies.json, remove historical 'tested on' --- .github/generate-sbom.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/generate-sbom.py b/.github/generate-sbom.py index fb9b37f27..c041300f2 100755 --- a/.github/generate-sbom.py +++ b/.github/generate-sbom.py @@ -26,6 +26,11 @@ def get_version() -> str: return version_file.read_text(encoding="utf-8").split('"')[1] +def load_dep_versions() -> dict[str, str]: + deps_file = Path(__file__).parent / "dependencies.json" + return json.loads(deps_file.read_text(encoding="utf-8")) + + def sha256_file(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() @@ -58,6 +63,7 @@ def generate(version: str) -> dict: purl = f"pkg:pypi/pillow@{version}" root = Path(__file__).parent.parent thirdparty = root / "src" / "thirdparty" + versions = load_dep_versions() metadata_component = { "bom-ref": purl, @@ -243,6 +249,7 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/freetype2", "type": "library", "name": "FreeType", + "version": versions["freetype"], "scope": "optional", "description": "Font rendering (optional, used by PIL._imagingft). " "Required for text/font support.", @@ -259,6 +266,7 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/fribidi", "type": "library", "name": "FriBiDi", + "version": versions["fribidi"], "scope": "optional", "description": "Unicode bidi algorithm library (optional, " "loaded at runtime by fribidi-shim).", @@ -275,6 +283,7 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/harfbuzz", "type": "library", "name": "HarfBuzz", + "version": versions["harfbuzz"], "scope": "optional", "description": "Text shaping (optional, required by libraqm " "for complex text layout).", @@ -291,9 +300,9 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/libavif", "type": "library", "name": "libavif", + "version": versions["libavif"], "scope": "optional", - "description": "AVIF codec (optional, used by PIL._avif). " - "Requires libavif >= 1.0.0.", + "description": "AVIF codec (optional, used by PIL._avif).", "licenses": [{"license": {"id": "BSD-2-Clause"}}], "externalReferences": [ {"type": "website", "url": "https://github.com/AOMediaCodec/libavif"}, @@ -307,9 +316,9 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/libimagequant", "type": "library", "name": "libimagequant", + "version": versions["libimagequant"], "scope": "optional", - "description": "Improved colour quantization (optional). " - "Tested with 2.6-4.4.1.", + "description": "Improved colour quantization (optional).", "licenses": [{"license": {"id": "GPL-3.0-or-later"}}], "externalReferences": [ {"type": "website", "url": "https://pngquant.org/lib/"}, @@ -323,9 +332,9 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/libjpeg", "type": "library", "name": "libjpeg / libjpeg-turbo", + "version": versions["jpegturbo"], "description": "JPEG codec (required by default; disable with " - "-C jpeg=disable). Tested with libjpeg 6b/8/9-9d " - "and libjpeg-turbo 2-3.", + "-C jpeg=disable).", "licenses": [ {"license": {"id": "IJG"}}, {"license": {"id": "BSD-3-Clause"}}, @@ -343,8 +352,9 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/libtiff", "type": "library", "name": "libtiff", + "version": versions["tiff"], "scope": "optional", - "description": "TIFF codec (optional). Tested with libtiff 4.0-4.7.1.", + "description": "TIFF codec (optional).", "licenses": [{"license": {"id": "libtiff"}}], "externalReferences": [ {"type": "website", "url": "https://libtiff.gitlab.io/libtiff/"}, @@ -358,6 +368,7 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/libwebp", "type": "library", "name": "libwebp", + "version": versions["libwebp"], "scope": "optional", "description": "WebP codec (optional, used by PIL._webp).", "licenses": [{"license": {"id": "BSD-3-Clause"}}], @@ -376,6 +387,7 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/libxcb", "type": "library", "name": "libxcb", + "version": versions["libxcb"], "scope": "optional", "description": "X11 screen-grab support (optional, " "used by PIL._imaging on macOS and Linux).", @@ -392,9 +404,9 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/littlecms2", "type": "library", "name": "Little CMS 2", + "version": versions["lcms2"], "scope": "optional", - "description": "Colour management (optional, used by PIL._imagingcms). " - "Tested with lcms2 2.7-2.18.", + "description": "Colour management (optional, used by PIL._imagingcms).", "licenses": [{"license": {"id": "MIT"}}], "externalReferences": [ {"type": "website", "url": "https://www.littlecms.com"}, @@ -408,9 +420,9 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/openjpeg", "type": "library", "name": "OpenJPEG", + "version": versions["openjpeg"], "scope": "optional", - "description": "JPEG 2000 codec (optional). " - "Tested with openjpeg 2.0.0-2.5.4.", + "description": "JPEG 2000 codec (optional).", "licenses": [{"license": {"id": "BSD-2-Clause"}}], "externalReferences": [ {"type": "website", "url": "https://www.openjpeg.org"}, @@ -439,6 +451,7 @@ def generate(version: str) -> dict: "bom-ref": "pkg:generic/zlib", "type": "library", "name": "zlib", + "version": versions["zlib-ng"], "description": "Deflate/PNG compression (required by default; " "disable with -C zlib=disable).", "licenses": [{"license": {"id": "Zlib"}}], From 3dda1d190f2136335eb10ff99987440d8743bf2e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:19:55 +0300 Subject: [PATCH 68/83] Git ignore generated SBOM --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3033c2ea7..4e9803957 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,6 @@ pillow-test-images.zip # pyinstaller *.spec + +# Generated SBOM +pillow-*.cdx.json From 0ef81c33af3d4416feb96a0b1cd8c2d3b3d06ab7 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:30:17 +1000 Subject: [PATCH 69/83] Add Fedora 44 (#9594) --- .github/workflows/test-docker.yml | 1 + docs/installation/platform-support.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index b035ac1de..e868b53a8 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -45,6 +45,7 @@ jobs: debian-13-trixie-x86, debian-13-trixie-amd64, fedora-43-amd64, + fedora-44-amd64, gentoo, ubuntu-22.04-jammy-amd64, ubuntu-24.04-noble-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index e90d989a2..90321d054 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,6 +31,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 43 | 3.14 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 44 | 3.14 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 | From 1f3b8a831d9cd7e140081289259fd0cb5d90934f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Apr 2026 00:13:37 +1000 Subject: [PATCH 70/83] If PdfParser buffer is memoryview, release it when closing --- src/PIL/PdfParser.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 2a5ade773..b0b32b1c9 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -383,7 +383,7 @@ class PdfParser: msg = "specify buf or f or filename, but not both buf and f" raise RuntimeError(msg) self.filename = filename - self.buf: bytes | bytearray | mmap.mmap | None = buf + self.buf: bytes | bytearray | memoryview | mmap.mmap | None = buf self.f = f self.start_offset = start_offset self.should_close_buf = False @@ -435,7 +435,9 @@ class PdfParser: self.seek_end() def close_buf(self) -> None: - if isinstance(self.buf, mmap.mmap): + if isinstance(self.buf, memoryview): + self.buf.release() + elif isinstance(self.buf, mmap.mmap): self.buf.close() self.buf = None From 4af29fb7324cd05cd8b6f6bbf1ff2dddf0a6c573 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Apr 2026 18:41:41 +1000 Subject: [PATCH 71/83] Restrict SBOM upload to Pillow JSON --- .github/workflows/wheels.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 98733b6c7..d5af65c98 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -294,12 +294,12 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: sbom - path: "*.cdx.json" + path: "pillow-*.cdx.json" - name: Validate SBOM run: | python3 -m pip install -r .ci/requirements-sbom.txt - check-jsonschema --schemafile "https://raw.githubusercontent.com/CycloneDX/specification/1.7/schema/bom-1.7.schema.json" *.cdx.json + check-jsonschema --schemafile "https://raw.githubusercontent.com/CycloneDX/specification/1.7/schema/bom-1.7.schema.json" pillow-*.cdx.json sbom-publish: if: | @@ -320,7 +320,7 @@ jobs: - name: Attach SBOM to GitHub release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release upload "$GITHUB_REF_NAME" *.cdx.json + run: gh release upload "$GITHUB_REF_NAME" pillow-*.cdx.json pypi-publish: if: github.event.repository.fork == false && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') From fc47d0760381fd904e4db45295912ef01d7137ae Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:17:39 +0300 Subject: [PATCH 72/83] No need to sort a sorted list --- .github/generate-sbom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/generate-sbom.py b/.github/generate-sbom.py index c041300f2..3b15a7d91 100755 --- a/.github/generate-sbom.py +++ b/.github/generate-sbom.py @@ -465,7 +465,7 @@ def generate(version: str) -> dict: dependencies = [ { "ref": purl, - "dependsOn": sorted(e["bom-ref"] for e in ext_components), + "dependsOn": [e["bom-ref"] for e in ext_components], }, { "ref": f"{purl}#c-ext/PIL._avif", From 7e4ca8b3abf1476b0c956bbc3642d0b2d5717e7c Mon Sep 17 00:00:00 2001 From: Hayato Ikoma Date: Fri, 1 May 2026 21:36:20 -0700 Subject: [PATCH 73/83] Correct integer overflow in 16-bit resampling (#9480) Co-authored-by: Andrew Murray --- Tests/test_image_resample.py | 34 ++++++++++++++++++++++++++++++++++ src/libImaging/Convert.c | 2 -- src/libImaging/ImagingUtils.h | 2 ++ src/libImaging/Resample.c | 12 ++++++------ 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 73b25ed51..f188be81e 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -627,3 +627,37 @@ class TestCoreResampleBox: 0.4, f">>> {size} {box} {flt}", ) + + +class TestCoreResample16bpc: + # Lanczos weighting during downsampling can push accumulated float sums + @pytest.mark.parametrize( + "offset", + ( + # below 0. These must be clamped to 0, not corrupted byte-by-byte. + 0, # Left half = 65535, right half = 0 + # above 65535. These must be clamped to 65535, not corrupted byte-by-byte. + 50, # # Left half = 0, right half = 65535 + ), + ) + def test_resampling_clamp_overflow(self, offset: int) -> None: + ims = {} + width, height = 100, 10 + for mode in ("I;16", "F"): + im = Image.new(mode, (width, height)) + im.paste(65535, (offset, 0, offset + width // 2, height)) + + # 5x downsampling with Lanczos + # creates ~8.7% overshoot or undershoot at the step edge + ims[mode] = im.resize((20, height), Image.Resampling.LANCZOS) + + for y in range(height): + for x in range(20): + v = ims["F"].getpixel((x, y)) + assert isinstance(v, float) + expected = max(0, min(65535, round(v))) + + value = ims["I;16"].getpixel((x, y)) + assert ( + value == expected + ), f"Pixel ({x}, {y}): expected {expected}, got {value}" diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 002497c32..f156810ff 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -37,8 +37,6 @@ #define MAX(a, b) (a) > (b) ? (a) : (b) #define MIN(a, b) (a) < (b) ? (a) : (b) -#define CLIP16(v) ((v) <= 0 ? 0 : (v) >= 65535 ? 65535 : (v)) - /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ #define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114) #define L24(rgb) ((rgb)[0] * 19595 + (rgb)[1] * 38470 + (rgb)[2] * 7471 + 0x8000) diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index 714458ad0..a362780d0 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -27,6 +27,8 @@ #define CLIP8(v) ((v) <= 0 ? 0 : (v) < 256 ? (v) : 255) +#define CLIP16(v) ((v) <= 0 ? 0 : (v) < 65536 ? (v) : 65535) + /* This is to work around a bug in GCC prior 4.9 in 64 bit mode. GCC generates code with partial dependency which is 3 times slower. See: https://stackoverflow.com/a/26588074/253146 */ diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index fea00eea0..1647dca14 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -492,9 +492,9 @@ ImagingResampleHorizontal_16bpc( << 8)) * k[x]; } - ss_int = ROUND_UP(ss); - imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256); - imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8); + ss_int = CLIP16(ROUND_UP(ss)); + imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF; + imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8; } } ImagingSectionLeave(&cookie); @@ -531,9 +531,9 @@ ImagingResampleVertical_16bpc( (imIn->image8[y + ymin][xx * 2 + (bigendian ? 0 : 1)] << 8)) * k[y]; } - ss_int = ROUND_UP(ss); - imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256); - imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8); + ss_int = CLIP16(ROUND_UP(ss)); + imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF; + imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8; } } ImagingSectionLeave(&cookie); From 2d02654c54c1584980fd239b58fefa7f1f8f4626 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 14:11:33 +1000 Subject: [PATCH 74/83] Update dependency cibuildwheel to v3.4.1 (#9607) --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index fd4183aff..c824c10bc 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.4.0 +cibuildwheel==3.4.1 From d92b826c4a4fff179b1f8c1ec421fefd78cdb26f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 06:03:07 +0000 Subject: [PATCH 75/83] Update github-actions --- .github/workflows/cifuzz.yml | 4 ++-- .github/workflows/lint.yml | 2 +- .github/workflows/wheels.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 27b55cffc..a2e1112dc 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -30,14 +30,14 @@ jobs: steps: - name: Build Fuzzers id: build - uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@e41e2f295eb18d630932fdd33d072527ba74c87b # master + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@c11174f47deee98f260dede5d661614bda78ae39 # master with: oss-fuzz-project-name: 'pillow' language: python dry-run: false - name: Run Fuzzers id: run - uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@e41e2f295eb18d630932fdd33d072527ba74c87b # master + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@c11174f47deee98f260dede5d661614bda78ae39 # master with: oss-fuzz-project-name: 'pillow' fuzz-seconds: 600 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1aff5a0dd..dacf40cc1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,7 +25,7 @@ jobs: with: python-version: "3.x" - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Lint run: uvx --with tox-uv tox -e lint - name: Mypy diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d5af65c98..e0edb3ac0 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -270,7 +270,7 @@ jobs: path: dist merge-multiple: true - name: Upload wheels to scientific-python-nightly-wheels - uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3 + uses: scientific-python/upload-nightly-action@e76cfec8a4611fd02808a801b0ff5a7d7c1b2d99 # 0.6.4 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} From 3bbb7a2a04c748b70ff1061572c67099a787ecc9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 10:25:22 +0000 Subject: [PATCH 76/83] Update dependency libpng to v1.6.58 --- .github/dependencies.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependencies.json b/.github/dependencies.json index 0f61b7817..a5e9b01f7 100644 --- a/.github/dependencies.json +++ b/.github/dependencies.json @@ -8,7 +8,7 @@ "lcms2": "2.18", "libavif": "1.4.1", "libimagequant": "4.4.1", - "libpng": "1.6.56", + "libpng": "1.6.58", "libwebp": "1.6.0", "libxcb": "1.17.0", "openjpeg": "2.5.4", From 956d434c68ee83f971057d4ce1321d12c12390be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 10:25:27 +0000 Subject: [PATCH 77/83] Update dependency lcms2 to v2.19 --- .github/dependencies.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependencies.json b/.github/dependencies.json index 0f61b7817..cf4990c67 100644 --- a/.github/dependencies.json +++ b/.github/dependencies.json @@ -5,7 +5,7 @@ "fribidi": "1.0.16", "harfbuzz": "13.2.1", "jpegturbo": "3.1.4.1", - "lcms2": "2.18", + "lcms2": "2.19", "libavif": "1.4.1", "libimagequant": "4.4.1", "libpng": "1.6.56", From 32b6c5f0eee19ccb5e255e1edfdf7fd8833edfa7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 10:25:32 +0000 Subject: [PATCH 78/83] Update dependency harfbuzz to v14 --- .github/dependencies.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependencies.json b/.github/dependencies.json index 0f61b7817..c21c60e9e 100644 --- a/.github/dependencies.json +++ b/.github/dependencies.json @@ -3,7 +3,7 @@ "bzip2": "1.0.8", "freetype": "2.14.3", "fribidi": "1.0.16", - "harfbuzz": "13.2.1", + "harfbuzz": "14.2.0", "jpegturbo": "3.1.4.1", "lcms2": "2.18", "libavif": "1.4.1", From 575b33d811b8f50577fb8465d0f59e7bca4e5d95 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:29:50 +0000 Subject: [PATCH 79/83] Update dependency mypy to v1.20.2 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index c64343a73..ad78bcb67 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.19.1 +mypy==1.20.2 arro3-compute arro3-core IceSpringPySideStubs-PyQt6 From c234720acad29ff0ccd10b1564c0cdaae1d0fade Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 May 2026 11:40:49 +1000 Subject: [PATCH 80/83] Convert Exif to dictionary before checking --- Tests/test_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 32c799195..81bd47299 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -862,7 +862,7 @@ class TestImage: def test_exif_webp(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.webp") as im: exif = im.getexif() - assert exif == {} + assert dict(exif) == {} out = tmp_path / "temp.webp" exif[258] = 8 @@ -884,7 +884,7 @@ class TestImage: def test_exif_png(self, tmp_path: Path) -> None: with Image.open("Tests/images/exif.png") as im: exif = im.getexif() - assert exif == {274: 1} + assert dict(exif) == {274: 1} out = tmp_path / "temp.png" exif[258] = 8 From 21790fc0da70f8dc9594248a1e30654c3cc05e65 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 May 2026 11:42:27 +1000 Subject: [PATCH 81/83] Check if sys.stdout is a TextIOWrapper instance --- src/PIL/Image.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 574980771..81add2f7a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2639,11 +2639,8 @@ class Image: if is_path(fp): filename = os.fspath(fp) open_fp = True - elif fp == sys.stdout: - try: - fp = sys.stdout.buffer - except AttributeError: - pass + elif fp == sys.stdout and isinstance(sys.stdout, io.TextIOWrapper): + fp = sys.stdout.buffer if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes filename = os.fspath(fp.name) From 4bba24632f13d2427ad7f52bf3402e35fe220b5f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 3 May 2026 22:13:11 +1000 Subject: [PATCH 82/83] Update docs --- docs/installation/building-from-source.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 79d54145a..ecb892e1f 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -51,7 +51,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.18**. + above uses liblcms2. Tested with **1.19** and **2.7-2.19**. * **libwebp** provides the WebP format. From ab25042353a72e7d75260bbacad71a3f942a7a4a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 4 May 2026 19:42:55 +1000 Subject: [PATCH 83/83] Set prCreation to not-pending --- .github/renovate.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json b/.github/renovate.json index f5af3d05a..387630dd6 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -7,6 +7,7 @@ "Dependency" ], "minimumReleaseAge": "7 days", + "prCreation": "not-pending", "schedule": [ "* * 3 * *" ],