CVE-2026-27809
psd-tools: Compression module has unguarded zlib decompression, missing dimension validation, and hardening gaps
CVE-2026-27809 is a medium vulnerability (CVSS 6.8) in Psd-tools, published February 25, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.48%, the 41st percentile. Fixed in psd-tools 1.12.2.
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.12.2, when a PSD file contains malformed RLE-compressed image data (e.g. a literal run that extends past the expected row size), decode_rle() raises ValueError which propagated all the way to the user, crashing psd.composite() and psd-tools export. decompress() already had a fallback that replaces failed channels with black pixels when result is None, but it never triggered because the ValueError from decode_rle() was not caught. The fix in version 1.12.2 wraps the decode_rle() call in a try/except so the existing fallback handles the error gracefully.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- High
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
6.8
psd-tools: Compression module has unguarded zlib decompression, missing dimension validation, and hardening gaps
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.12.2, when a PSD file contains malformed RLE-compressed image data (e.g. a literal run that extends past the expected row size), decode_rle() raises ValueError which propagated all the way to the user, crashing psd.composite() and psd-tools export. decompress() already had a fallback that replaces failed channels with black pixels when result is None, but it never triggered because the ValueError from decode_rle() was not caught. The fix in version 1.12.2 wraps the decode_rle() call in a try/except so the existing fallback handles the error gracefully.
- Record
CVE-2026-27809- CVSS 4.0
- 6.8 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:U - Weakness
- CWE-409, CWE-789, CWE-617, CWE-190, CWE-755, CWE-704
- Updated
- 2026-11-02
EUVDEUVD-2026-8781
6.8
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.12.2, when a PSD file contains malformed RLE-compressed image data (e.g. a literal run that extends past the expected row size), decode_rle() raises ValueError which propagated all the way to the user, crashing psd.composite() and psd-tools export. decompress() already had a fallback that replaces failed channels with black pixels when result is None, but it never triggered because the ValueError from decode_rle() was not caught. The fix in version 1.12.2 wraps the decode_rle() call in a try/except so the existing fallback handles the error gracefully.
- Record
EUVD-2026-8781- CVSS 4.0
- 6.8 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:U - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-24p2-j2jr-386w
6.8
psd-tools: Compression module has unguarded zlib decompression, missing dimension validation, and hardening gaps
Summary
A security review of the psd_tools.compression module (conducted against the fix/invalid-rle-compression branch, commits 7490ffa–2a006f5) identified the following pre-existing issues. The two findings introduced and fixed by those commits (Cython buffer overflow, IndexError on lone repeat header) are excluded from this report.
Findings
1. Unguarded zlib.decompress — ZIP bomb / memory exhaustion (Medium)
Location: src/psd_tools/compression/__init__.py, lines 159 and 162
result = zlib.decompress(data) # Compression.ZIP
decompressed = zlib.decompress(data) # Compression.ZIP_WITH_PREDICTION
zlib.decompress is called without a max_length cap. A crafted PSD file containing a ZIP-compressed channel whose compressed payload expands to gigabytes would exhaust process memory before any limit is enforced. The RLE path is not vulnerable to this because the decoder pre-allocates exactly row_size × height bytes; the ZIP path has no equivalent ceiling.
Impact: Denial-of-service / OOM crash when processing untrusted PSD files.
Suggested mitigation: Pass a reasonable max_length to zlib.decompress, derived from the expected width * height * depth // 8 byte count already computed in decompress().
2. No upper-bound validation on image dimensions before allocation (Low)
Location: src/psd_tools/compression/__init__.py, lines 138 and 193
length = width * height * max(1, depth // 8) # decompress()
row_size = max(width * depth // 8, 1) # decode_rle()
Neither width, height, nor depth are range-checked before these values drive memory allocation. The PSD format (version 2 / PSB) permits dimensions up to 300,000 × 300,000 pixels; a 4-channel 32-bit image at that size would require ~144 TB to hold. While the OS/Python allocator will reject such a request, there is no early, explicit guard that produces a clean, user-facing error.
Impact: Uncontrolled allocation attempt from a malformed or adversarially crafted PSB file; hard crash rather than a recoverable error.
Suggested mitigation: Validate width, height, and depth against known PSD/PSB limits before entering decompression, and raise a descriptive ValueError early.
3. assert used as a runtime integrity check (Low)
Location: src/psd_tools/compression/__init__.py, line 170
assert len(result) == length, "len=%d, expected=%d" % (len(result), length)
This assertion can be silently disabled by running the interpreter with -O (or -OO), which strips all assert statements. If the assertion ever becomes relevant (e.g., after future refactoring), disabling it would allow a length mismatch to propagate silently into downstream image compositing.
Impact: Loss of an integrity guard in optimised deployments.
Suggested mitigation: Replace with an explicit if + raise ValueError(...).
4. cdef int indices vs. Py_ssize_t size type mismatch in Cython decoder (Low)
Location: src/psd_tools/compression/_rle.pyx, lines 18–20
cdef int i = 0
cdef int j = 0
cdef int length = data.shape[0]
All loop indices are C signed int (32-bit). The size parameter is Py_ssize_t (64-bit on modern platforms). The comparison j < size promotes j to Py_ssize_t, but if j wraps due to a row size exceeding INT_MAX (~2.1 GB), the resulting comparison is undefined behaviour in C. In practice, row sizes are bounded by PSD/PSB dimension limits and are unreachable at this scale; however, the mismatch is a latent defect if the function is ever called directly with large synthetic inputs.
Impact: Theoretical infinite loop or UB at >2 GB row sizes; not reachable from standard PSD/PSB parsing.
Suggested mitigation: Change cdef int i, j, length to cdef Py_ssize_t.
5. Silent data degradation not surfaced to callers (Informational)
Location:…
- Record
GHSA-24p2-j2jr-386w- CVSS 4.0
- 6.8 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:U - Weakness
- CWE-190, CWE-409, CWE-617, CWE-704, CWE-755, CWE-789
- Updated
- 2026-11-02
NVD
6.8
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.12.2, when a PSD file contains malformed RLE-compressed image data (e.g. a literal run that extends past the expected row size), decode_rle() raises ValueError which propagated all the way to the user, crashing psd.composite() and psd-tools export. decompress() already had a fallback that replaces failed channels with black pixels when result is None, but it never triggered because the ValueError from decode_rle() was not caught. The fix in version 1.12.2 wraps the decode_rle() call in a try/except so the existing fallback handles the error gracefully.
- Record
CVE-2026-27809- CVSS 4.0
- 6.8 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:U/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 9.1 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H - Weakness
- CWE-190, CWE-409, CWE-617, CWE-704, CWE-755, CWE-789
- Updated
- 2026-11-02
OSV
6.8
psd-tools: Compression module has unguarded zlib decompression, missing dimension validation, and hardening gaps
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.12.2, when a PSD file contains malformed RLE-compressed image data (e.g. a literal run that extends past the expected row size), decode_rle() raises ValueError which propagated all the way to the user, crashing psd.composite() and psd-tools export. decompress() already had a fallback that replaces failed channels with black pixels when result is None, but it never triggered because the ValueError from decode_rle() was not caught. The fix in version 1.12.2 wraps the decode_rle() call in a try/except so the existing fallback handles the error gracefully.
- Record
CVE-2026-27809- CVSS 4.0
- 6.8 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:U - Weakness
- CWE-190, CWE-409, CWE-617, CWE-704, CWE-755, CWE-789
- Updated
- 2026-11-02
OSVDEBIAN-CVE-2026-27809
9.1
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.12.2, when a PSD file contains malformed RLE-compressed image data (e.g. a literal run that extends past the expected row size), decode_rle() raises ValueError which propagated all the way to the user, crashing psd.composite() and psd-tools export. decompress() already had a fallback that replaces failed channels with black pixels when result is None, but it never triggered because the ValueError from decode_rle() was not caught. The fix in version 1.12.2 wraps the decode_rle() call in a try/except so the existing fallback handles the error gracefully.
- Record
DEBIAN-CVE-2026-27809- CVSS 3.1
- 9.1 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H - Updated
- 2026-11-02
OSVGHSA-24p2-j2jr-386w
6.8
psd-tools: Compression module has unguarded zlib decompression, missing dimension validation, and hardening gaps
Summary
A security review of the psd_tools.compression module (conducted against the fix/invalid-rle-compression branch, commits 7490ffa–2a006f5) identified the following pre-existing issues. The two findings introduced and fixed by those commits (Cython buffer overflow, IndexError on lone repeat header) are excluded from this report.
Findings
1. Unguarded zlib.decompress — ZIP bomb / memory exhaustion (Medium)
Location: src/psd_tools/compression/__init__.py, lines 159 and 162
result = zlib.decompress(data) # Compression.ZIP
decompressed = zlib.decompress(data) # Compression.ZIP_WITH_PREDICTION
zlib.decompress is called without a max_length cap. A crafted PSD file containing a ZIP-compressed channel whose compressed payload expands to gigabytes would exhaust process memory before any limit is enforced. The RLE path is not vulnerable to this because the decoder pre-allocates exactly row_size × height bytes; the ZIP path has no equivalent ceiling.
Impact: Denial-of-service / OOM crash when processing untrusted PSD files.
Suggested mitigation: Pass a reasonable max_length to zlib.decompress, derived from the expected width * height * depth // 8 byte count already computed in decompress().
2. No upper-bound validation on image dimensions before allocation (Low)
Location: src/psd_tools/compression/__init__.py, lines 138 and 193
length = width * height * max(1, depth // 8) # decompress()
row_size = max(width * depth // 8, 1) # decode_rle()
Neither width, height, nor depth are range-checked before these values drive memory allocation. The PSD format (version 2 / PSB) permits dimensions up to 300,000 × 300,000 pixels; a 4-channel 32-bit image at that size would require ~144 TB to hold. While the OS/Python allocator will reject such a request, there is no early, explicit guard that produces a clean, user-facing error.
Impact: Uncontrolled allocation attempt from a malformed or adversarially crafted PSB file; hard crash rather than a recoverable error.
Suggested mitigation: Validate width, height, and depth against known PSD/PSB limits before entering decompression, and raise a descriptive ValueError early.
3. assert used as a runtime integrity check (Low)
Location: src/psd_tools/compression/__init__.py, line 170
assert len(result) == length, "len=%d, expected=%d" % (len(result), length)
This assertion can be silently disabled by running the interpreter with -O (or -OO), which strips all assert statements. If the assertion ever becomes relevant (e.g., after future refactoring), disabling it would allow a length mismatch to propagate silently into downstream image compositing.
Impact: Loss of an integrity guard in optimised deployments.
Suggested mitigation: Replace with an explicit if + raise ValueError(...).
4. cdef int indices vs. Py_ssize_t size type mismatch in Cython decoder (Low)
Location: src/psd_tools/compression/_rle.pyx, lines 18–20
cdef int i = 0
cdef int j = 0
cdef int length = data.shape[0]
All loop indices are C signed int (32-bit). The size parameter is Py_ssize_t (64-bit on modern platforms). The comparison j < size promotes j to Py_ssize_t, but if j wraps due to a row size exceeding INT_MAX (~2.1 GB), the resulting comparison is undefined behaviour in C. In practice, row sizes are bounded by PSD/PSB dimension limits and are unreachable at this scale; however, the mismatch is a latent defect if the function is ever called directly with large synthetic inputs.
Impact: Theoretical infinite loop or UB at >2 GB row sizes; not reachable from standard PSD/PSB parsing.
Suggested mitigation: Change cdef int i, j, length to cdef Py_ssize_t.
5. Silent data degradation not surfaced to callers (Informational)
Location:…
- Record
GHSA-24p2-j2jr-386w- CVSS 4.0
- 6.8 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:U - Weakness
- CWE-190, CWE-409, CWE-617, CWE-704, CWE-755, CWE-789
- Updated
- 2026-11-02
OSVPYSEC-2026-2262
9.1
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.12.2, when a PSD file contains malformed RLE-compressed image data (e.g. a literal run that extends past the expected row size), decode_rle() raises ValueError which propagated all the way to the user, crashing psd.composite() and psd-tools export. decompress() already had a fallback that replaces failed channels with black pixels when result is None, but it never triggered because the ValueError from decode_rle() was not caught. The fix in version 1.12.2 wraps the decode_rle() call in a try/except so the existing fallback handles the error gracefully.
- Record
PYSEC-2026-2262- CVSS 3.1
- 9.1 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H - Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-27809
6.8
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.12.2, when a PSD file contains malformed RLE-compressed image data (e.g. a literal run that extends past the expected row size), decode_rle() raises ValueError which propagated all the way to the user, crashing psd.composite() and psd-tools export. decompress() already had a fallback that replaces failed channels with black pixels when result is None, but it never triggered because the ValueError from decode_rle() was not caught. The fix in version 1.12.2 wraps the decode_rle() call in a try/except so the existing fallback handles the error gracefully.
- Record
UBUNTU-CVE-2026-27809- CVSS 4.0
- 6.8 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:U - CVSS 3.1
- 9.1 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
psd-toolsPyPI |
before 1.12.2 | 1.12.2 |
psd-toolsDebian:12 |
All versions, plus listed versions 1.10.7+dfsg.1-11.10.9+dfsg.1-11.12.0+dfsg.1-11.12.1+dfsg.1-11.14.2+dfsg.1-1 Show 11 moreShow less1.14.3+dfsg.1-11.15.0+dfsg.1-11.16.0+dfsg.1-11.17.0+dfsg.1-11.17.4+dfsg.1-11.9.24+dfsg.1-11.9.28+dfsg.1-11.9.30+dfsg.1-11.9.31+dfsg.1-11.9.32+dfsg.1-11.9.34+dfsg.1-1 |
None known |
psd-toolsDebian:13 |
before 1.10.7+dfsg.1-1+deb13u1, plus listed versions 1.10.7+dfsg.1-1 |
1.10.7+dfsg.1-1+deb13u1 |
psd-toolsDebian:14 |
before 1.14.2+dfsg.1-1, plus listed versions 1.10.7+dfsg.1-11.10.9+dfsg.1-11.12.0+dfsg.1-11.12.1+dfsg.1-1 |
1.14.2+dfsg.1-1 |
psd-toolsPyPI |
before 1.12.2, plus listed versions 0.1.10.1.20.1.30.1.40.10 Show 45 moreShow less0.20.50.60.70.7.10.80.8.10.8.20.8.30.8.40.90.9.11.01.11.10.01.10.11.10.101.10.111.10.121.10.131.10.21.10.31.10.41.10.51.10.61.10.71.10.81.10.91.11.01.11.11.12.01.12.11.21.31.41.8.101.8.111.8.121.8.131.8.141.8.151.8.161.8.171.8.181.8.19 |
1.12.2 |
psd-toolsUbuntu:24.04:LTS |
All versions, plus listed versions 1.9.28+dfsg.1-11.9.28+dfsg.1-1build11.9.30+dfsg.1-11.9.30+dfsg.1-1build1 |
None known |
psd-toolsUbuntu:25.10 |
All versions, plus listed versions 1.9.34+dfsg.1-1build21.10.7+dfsg.1-1 |
None known |
psd-toolsUbuntu:26.10 |
All versions, plus listed versions 1.12.1+dfsg.1-11.17.0+dfsg.1-11.17.4+dfsg.1-1 |
None known |
psd-toolsUbuntu:26.04:LTS |
All versions, plus listed versions 1.10.7+dfsg.1-11.10.7+dfsg.1-1build11.10.9+dfsg.1-11.12.0+dfsg.1-11.12.1+dfsg.1-1 |
None known |
History
| When | Changed | Sources |
|---|---|---|
| 2026-11-02 | created | CVE List, EUVD, GitHub Advisories, NVD, OSV |
References
github.com/psd-tools/psd-tools/security/advisories/GHSA-24p2-j2jr-386w
github.com/psd-tools/psd-tools/commit/6c0a78f195b5942757886a1863793fd5946c1fb1
github.com/psd-tools/psd-tools/releases/tag/v1.12.2
nvd.nist.gov/vuln/detail/CVE-2026-27809
github.com/advisories/GHSA-24p2-j2jr-386w
github.com/CVEProject/cvelistV5/tree/main/cves/2026/27xxx/CVE-2026-27809.json
Left out here: 55 versions and the rest of 2 source descriptions. The JSON record has them.