CVE-2026-49836
psd-tools: arbitrary file write via smart-object filename
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.17.1, SmartObject.save() writes an embedded smart object to a path taken verbatim from the PSD file. Because that name is attacker-controlled and unsanitised, a tool that extracts embedded objects from an untrusted .psd can be made to write attacker-chosen bytes to an attacker-chosen path (absolute or ../-traversing), outside its intended output directory. A secondary issue in SmartObject.open() for external-kind smart objects allows the attacker-controlled fullPath descriptor to be used as an arbitrary file read path, enabling exfiltration of the read content to the controlled write destination. Both issues are fixed in v1.17.1.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- Low
- Privileges needed
- None
- User interaction
- Active
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
4.6
psd-tools: arbitrary file write via smart-object filename
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.17.1, SmartObject.save() writes an embedded smart object to a path taken verbatim from the PSD file. Because that name is attacker-controlled and unsanitised, a tool that extracts embedded objects from an untrusted .psd can be made to write attacker-chosen bytes to an attacker-chosen path (absolute or ../-traversing), outside its intended output directory. A secondary issue in SmartObject.open() for external-kind smart objects allows the attacker-controlled fullPath descriptor to be used as an arbitrary file read path, enabling exfiltration of the read content to the controlled write destination. Both issues are fixed in v1.17.1.
- Record
CVE-2026-49836- CVSS 4.0
- 4.6 Medium
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
EUVDEUVD-2026-75804
4.6
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.17.1, SmartObject.save() writes an embedded smart object to a path taken verbatim from the PSD file. Because that name is attacker-controlled and unsanitised, a tool that extracts embedded objects from an untrusted .psd can be made to write attacker-chosen bytes to an attacker-chosen path (absolute or ../-traversing), outside its intended output directory. A secondary issue in SmartObject.open() for external-kind smart objects allows the attacker-controlled fullPath descriptor to be used as an arbitrary file read path, enabling exfiltration of the read content to the controlled write destination. Both issues are fixed in v1.17.1.
- Record
EUVD-2026-75804- CVSS 4.0
- 4.6 Medium
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-2rmg-vrx8-9j2f
4.6
psd-tools vulnerable to arbitrary file write via smart-object filename
psd-tools: arbitrary file write/read via smart-object path traversal
Summary
In psd-tools (all releases exposing the SmartObject API through v1.17.0), SmartObject.save() writes an embedded smart object to a path taken verbatim from the PSD file. Because that name is attacker-controlled and unsanitised, a tool that extracts embedded objects from an untrusted .psd can be made to write attacker-chosen bytes to an attacker-chosen path (absolute or ../-traversing), outside its intended output directory.
A secondary issue in SmartObject.open() for external-kind smart objects allows the attacker-controlled fullPath descriptor to be used as an arbitrary file read path, enabling exfiltration of the read content to the controlled write destination. Both issues are fixed in v1.17.1.
Details
Write path — SmartObject.save() (primary)
src/psd_tools/api/smart_object.py:170-179 (tag v1.17.0):
def save(self, filename: str | None = None) -> None:
if filename is None:
filename = self.filename # untrusted, straight from the file
with open(filename, "wb") as f:
f.write(self.data) # attacker-controlled bytes
self.filename comes from the file with no validation — the filename property (:62-67) returns self._data.filename, set by the linked-layer parser at src/psd_tools/psd/linked_layer.py:100 (read_unicode_string(fp)). There is no basename, no absolute path rejection, and no .. filtering; the written contents (self.data) are likewise from the file, so the attacker controls both destination and content.
Read path — SmartObject.open() / .data for external kind (secondary)
For kind == "external", save() read file content via the data property, which called open() with no external_dir constraint. The fullPath descriptor embedded in the PSD was then used verbatim as the source path, enabling an attacker-crafted PSD to cause save(directory="/safe/out") to read an arbitrary readable file (e.g. /etc/passwd) and write its contents to the output directory.
Proof of concept
Standalone, against the released package (writes only into a fresh temp dir; exit 0 = confirmed). A Docker bundle is available on request.
pip install psd-tools==1.17.0
python poc.py
poc.py builds two PSDs from the project's own placedLayer.psd fixture (included as base.psd), differing only in the embedded smart-object name — control is a bare basename, exploit is ../../PWNED-psd-tools-poc.bin — then extracts each like a consumer would:
import os, shutil, tempfile
from psd_tools import PSDImage
from psd_tools.constants import Tag
MARKER = b"PSD-TOOLS-POC: arbitrary-file-write payload (attacker-controlled bytes)\n"
NAMES = {"control": "embedded-export.bin", "exploit": "../../PWNED-psd-tools-poc.bin"}
def craft(name, out):
psd = PSDImage.open(os.path.join(os.path.dirname(__file__), "base.psd"))
uuid = next(l.smart_object.unique_id for l in psd.descendants()
if l.kind == "smartobject" and l.smart_object.kind == "data")
for key in (Tag.LINKED_LAYER1, Tag.LINKED_LAYER2, Tag.LINKED_LAYER3, Tag.LINKED_LAYER_EXTERNAL):
for item in (psd.tagged_blocks.get_data(key) or []) if key in psd.tagged_blocks else []:
if item.uuid.strip("\x00") == uuid:
item.filename, item.data = name, MARKER
psd.save(out)
def extract(psd_path, outdir, watch):
psd = PSDImage.open(psd_path)
before = {os.path.realpath(os.path.join(d, f)) for d, _, fs in os.walk(watch) for f in fs}
cwd = os.getcwd(); os.chdir(outdir)
try:
for l in psd.descendants():
if l.kind == "smartobject" and l.smart_object.kind == "data":
l.smart_object.save()
finally:
os.chdir(cwd)
after = {os.path.realpath(os.path.join(d, f)) for d, _, fs in os.walk(watch) for f in fs}
return sorted(after - before)
def…
- Record
GHSA-2rmg-vrx8-9j2f- CVSS 4.0
- 4.6 Medium
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
NVD
4.6
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.17.1, SmartObject.save() writes an embedded smart object to a path taken verbatim from the PSD file. Because that name is attacker-controlled and unsanitised, a tool that extracts embedded objects from an untrusted .psd can be made to write attacker-chosen bytes to an attacker-chosen path (absolute or ../-traversing), outside its intended output directory. A secondary issue in SmartObject.open() for external-kind smart objects allows the attacker-controlled fullPath descriptor to be used as an arbitrary file read path, enabling exfiltration of the read content to the controlled write destination. Both issues are fixed in v1.17.1.
- Record
CVE-2026-49836- CVSS 4.0
- 4.6 Medium
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/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 - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
OSV
4.6
psd-tools: arbitrary file write via smart-object filename
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.17.1, SmartObject.save() writes an embedded smart object to a path taken verbatim from the PSD file. Because that name is attacker-controlled and unsanitised, a tool that extracts embedded objects from an untrusted .psd can be made to write attacker-chosen bytes to an attacker-chosen path (absolute or ../-traversing), outside its intended output directory. A secondary issue in SmartObject.open() for external-kind smart objects allows the attacker-controlled fullPath descriptor to be used as an arbitrary file read path, enabling exfiltration of the read content to the controlled write destination. Both issues are fixed in v1.17.1.
- Record
CVE-2026-49836- CVSS 4.0
- 4.6 Medium
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
OSVDEBIAN-CVE-2026-49836
4.6
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.17.1, SmartObject.save() writes an embedded smart object to a path taken verbatim from the PSD file. Because that name is attacker-controlled and unsanitised, a tool that extracts embedded objects from an untrusted .psd can be made to write attacker-chosen bytes to an attacker-chosen path (absolute or ../-traversing), outside its intended output directory. A secondary issue in SmartObject.open() for external-kind smart objects allows the attacker-controlled fullPath descriptor to be used as an arbitrary file read path, enabling exfiltration of the read content to the controlled write destination. Both issues are fixed in v1.17.1.
- Record
DEBIAN-CVE-2026-49836- CVSS 4.0
- 4.6 Medium
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/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 - Updated
- 2026-11-02
OSVGHSA-2rmg-vrx8-9j2f
4.6
psd-tools vulnerable to arbitrary file write via smart-object filename
psd-tools: arbitrary file write/read via smart-object path traversal
Summary
In psd-tools (all releases exposing the SmartObject API through v1.17.0), SmartObject.save() writes an embedded smart object to a path taken verbatim from the PSD file. Because that name is attacker-controlled and unsanitised, a tool that extracts embedded objects from an untrusted .psd can be made to write attacker-chosen bytes to an attacker-chosen path (absolute or ../-traversing), outside its intended output directory.
A secondary issue in SmartObject.open() for external-kind smart objects allows the attacker-controlled fullPath descriptor to be used as an arbitrary file read path, enabling exfiltration of the read content to the controlled write destination. Both issues are fixed in v1.17.1.
Details
Write path — SmartObject.save() (primary)
src/psd_tools/api/smart_object.py:170-179 (tag v1.17.0):
def save(self, filename: str | None = None) -> None:
if filename is None:
filename = self.filename # untrusted, straight from the file
with open(filename, "wb") as f:
f.write(self.data) # attacker-controlled bytes
self.filename comes from the file with no validation — the filename property (:62-67) returns self._data.filename, set by the linked-layer parser at src/psd_tools/psd/linked_layer.py:100 (read_unicode_string(fp)). There is no basename, no absolute path rejection, and no .. filtering; the written contents (self.data) are likewise from the file, so the attacker controls both destination and content.
Read path — SmartObject.open() / .data for external kind (secondary)
For kind == "external", save() read file content via the data property, which called open() with no external_dir constraint. The fullPath descriptor embedded in the PSD was then used verbatim as the source path, enabling an attacker-crafted PSD to cause save(directory="/safe/out") to read an arbitrary readable file (e.g. /etc/passwd) and write its contents to the output directory.
Proof of concept
Standalone, against the released package (writes only into a fresh temp dir; exit 0 = confirmed). A Docker bundle is available on request.
pip install psd-tools==1.17.0
python poc.py
poc.py builds two PSDs from the project's own placedLayer.psd fixture (included as base.psd), differing only in the embedded smart-object name — control is a bare basename, exploit is ../../PWNED-psd-tools-poc.bin — then extracts each like a consumer would:
import os, shutil, tempfile
from psd_tools import PSDImage
from psd_tools.constants import Tag
MARKER = b"PSD-TOOLS-POC: arbitrary-file-write payload (attacker-controlled bytes)\n"
NAMES = {"control": "embedded-export.bin", "exploit": "../../PWNED-psd-tools-poc.bin"}
def craft(name, out):
psd = PSDImage.open(os.path.join(os.path.dirname(__file__), "base.psd"))
uuid = next(l.smart_object.unique_id for l in psd.descendants()
if l.kind == "smartobject" and l.smart_object.kind == "data")
for key in (Tag.LINKED_LAYER1, Tag.LINKED_LAYER2, Tag.LINKED_LAYER3, Tag.LINKED_LAYER_EXTERNAL):
for item in (psd.tagged_blocks.get_data(key) or []) if key in psd.tagged_blocks else []:
if item.uuid.strip("\x00") == uuid:
item.filename, item.data = name, MARKER
psd.save(out)
def extract(psd_path, outdir, watch):
psd = PSDImage.open(psd_path)
before = {os.path.realpath(os.path.join(d, f)) for d, _, fs in os.walk(watch) for f in fs}
cwd = os.getcwd(); os.chdir(outdir)
try:
for l in psd.descendants():
if l.kind == "smartobject" and l.smart_object.kind == "data":
l.smart_object.save()
finally:
os.chdir(cwd)
after = {os.path.realpath(os.path.join(d, f)) for d, _, fs in os.walk(watch) for f in fs}
return sorted(after - before)
def…
- Record
GHSA-2rmg-vrx8-9j2f- CVSS 4.0
- 4.6 Medium
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
OSVPYSEC-2026-2969
4.6
psd-tools vulnerable to arbitrary file write via smart-object filename
psd-tools: arbitrary file write/read via smart-object path traversal
Summary
In psd-tools (all releases exposing the SmartObject API through v1.17.0), SmartObject.save() writes an embedded smart object to a path taken verbatim from the PSD file. Because that name is attacker-controlled and unsanitised, a tool that extracts embedded objects from an untrusted .psd can be made to write attacker-chosen bytes to an attacker-chosen path (absolute or ../-traversing), outside its intended output directory.
A secondary issue in SmartObject.open() for external-kind smart objects allows the attacker-controlled fullPath descriptor to be used as an arbitrary file read path, enabling exfiltration of the read content to the controlled write destination. Both issues are fixed in v1.17.1.
Details
Write path — SmartObject.save() (primary)
src/psd_tools/api/smart_object.py:170-179 (tag v1.17.0):
def save(self, filename: str | None = None) -> None:
if filename is None:
filename = self.filename # untrusted, straight from the file
with open(filename, "wb") as f:
f.write(self.data) # attacker-controlled bytes
self.filename comes from the file with no validation — the filename property (:62-67) returns self._data.filename, set by the linked-layer parser at src/psd_tools/psd/linked_layer.py:100 (read_unicode_string(fp)). There is no basename, no absolute path rejection, and no .. filtering; the written contents (self.data) are likewise from the file, so the attacker controls both destination and content.
Read path — SmartObject.open() / .data for external kind (secondary)
For kind == "external", save() read file content via the data property, which called open() with no external_dir constraint. The fullPath descriptor embedded in the PSD was then used verbatim as the source path, enabling an attacker-crafted PSD to cause save(directory="/safe/out") to read an arbitrary readable file (e.g. /etc/passwd) and write its contents to the output directory.
Proof of concept
Standalone, against the released package (writes only into a fresh temp dir; exit 0 = confirmed). A Docker bundle is available on request.
pip install psd-tools==1.17.0
python poc.py
poc.py builds two PSDs from the project's own placedLayer.psd fixture (included as base.psd), differing only in the embedded smart-object name — control is a bare basename, exploit is ../../PWNED-psd-tools-poc.bin — then extracts each like a consumer would:
import os, shutil, tempfile
from psd_tools import PSDImage
from psd_tools.constants import Tag
MARKER = b"PSD-TOOLS-POC: arbitrary-file-write payload (attacker-controlled bytes)\n"
NAMES = {"control": "embedded-export.bin", "exploit": "../../PWNED-psd-tools-poc.bin"}
def craft(name, out):
psd = PSDImage.open(os.path.join(os.path.dirname(__file__), "base.psd"))
uuid = next(l.smart_object.unique_id for l in psd.descendants()
if l.kind == "smartobject" and l.smart_object.kind == "data")
for key in (Tag.LINKED_LAYER1, Tag.LINKED_LAYER2, Tag.LINKED_LAYER3, Tag.LINKED_LAYER_EXTERNAL):
for item in (psd.tagged_blocks.get_data(key) or []) if key in psd.tagged_blocks else []:
if item.uuid.strip("\x00") == uuid:
item.filename, item.data = name, MARKER
psd.save(out)
def extract(psd_path, outdir, watch):
psd = PSDImage.open(psd_path)
before = {os.path.realpath(os.path.join(d, f)) for d, _, fs in os.walk(watch) for f in fs}
cwd = os.getcwd(); os.chdir(outdir)
try:
for l in psd.descendants():
if l.kind == "smartobject" and l.smart_object.kind == "data":
l.smart_object.save()
finally:
os.chdir(cwd)
after = {os.path.realpath(os.path.join(d, f)) for d, _, fs in os.walk(watch) for f in fs}
return sorted(after - before)
def…
- Record
PYSEC-2026-2969- CVSS 4.0
- 4.6 Medium
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-49836
4.6
psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.17.1, SmartObject.save() writes an embedded smart object to a path taken verbatim from the PSD file. Because that name is attacker-controlled and unsanitised, a tool that extracts embedded objects from an untrusted .psd can be made to write attacker-chosen bytes to an attacker-chosen path (absolute or ../-traversing), outside its intended output directory. A secondary issue in SmartObject.open() for external-kind smart objects allows the attacker-controlled fullPath descriptor to be used as an arbitrary file read path, enabling exfiltration of the read content to the controlled write destination. Both issues are fixed in v1.17.1.
- Record
UBUNTU-CVE-2026-49836- CVSS 4.0
- 4.6 Medium
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
psd-toolsPyPI |
through 1.17.0 | 1.17.1 |
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 |
All versions, plus listed versions 1.10.7+dfsg.1-11.10.7+dfsg.1-1+deb13u11.10.9+dfsg.1-11.12.0+dfsg.1-11.12.1+dfsg.1-1 Show 6 moreShow less1.14.2+dfsg.1-11.14.3+dfsg.1-11.15.0+dfsg.1-11.16.0+dfsg.1-11.17.0+dfsg.1-11.17.4+dfsg.1-1 |
None known |
psd-toolsDebian:14 |
before 1.17.4+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-11.14.2+dfsg.1-1 Show 4 moreShow less1.14.3+dfsg.1-11.15.0+dfsg.1-11.16.0+dfsg.1-11.17.0+dfsg.1-1 |
1.17.4+dfsg.1-1 |
psd-toolsPyPI |
before 1.17.1, 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.12.21.13.01.13.11.14.01.14.11.14.21.14.31.15.0.post11.16.01.17.01.21.31.4 |
1.17.1 |
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: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
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-49836 is a medium vulnerability (CVSS 4.6) in Psd-tools, published July 9, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.19%, the 8th percentile. Fixed in psd-tools 1.17.1.
References
github.com/psd-tools/psd-tools/security/advisories/GHSA-2rmg-vrx8-9j2f
github.com/psd-tools/psd-tools/pull/657
github.com/pypa/advisory-database/tree/main/vulns/psd-tools/PYSEC-2026-2969.yaml
github.com/psd-tools/psd-tools/releases/tag/v1.17.1
nvd.nist.gov/vuln/detail/CVE-2026-49836
github.com/advisories/GHSA-2rmg-vrx8-9j2f
Left out here: 65 versions and the rest of 3 source descriptions. The JSON record has them.