GHSA-2rmg-vrx8-9j2f psd-tools vulnerable to arbitrary file write via smart-object filename
psd-tools: arbitrary file write/read via smart-object path traversalSummaryIn `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**.DetailsWrite path — `SmartObject.save()` (primary)`src/psd_tools/api/smart_object.py:170-179` (tag `v1.17.0`):`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 conceptStandalone, against the released package (writes only into a fresh temp dir; exit 0 = confirmed). A Docker bundle is available on request.`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:Output (`psd-tools 1.17.0`):An absolute embedded name (e.g. `/home/user/.bashrc`) is honoured the same way.ImpactAny application that ingests untrusted PSD/PSB files and extracts their embedded smart objects via `SmartObject.save()` can be coerced into writing attacker-controlled bytes to an attacker-chosen existing directory — no authentication or special configuration required. High integrity impact; can escalate to code execution depending on the target path.For external-kind smart objects the same call additionally allowed arbitrary file reads, with the read content written to the controlled output directory.Severity**Moderate** for the common case (a library/desktop tool where a user initiates extraction). Higher for a service that auto-extracts smart objects from uploaded PSDs without user interaction.PatchFixed in **v1.17.1** (PR #657). Changes to `src/psd_tools/api/smart_object.py`:**`save()`**: strips directory components from the embedded name via `os.path.basename()`, writes only into a caller-supplied `directory` (defaults to CWD), and verifies the resolved path stays inside that directory via `os.path.realpath()` + `os.path.commonpath()`. A new `external_dir` parameter is propagated to `open()` for external-kind objects to constrain the read source.**`open()`**: when `external_dir` is provided, a `fullPath` resolving outside it is silently ignored (falls through to `relPath`); a `relPath` escaping the directory raises `ValueError`.WeaknessesCWE-22 (Improper Limitation of a Pathname to a Restricted Directory) via CWE-73 (External Control of File Name or Path).ResourcesFix PR: https://github.com/psd-tools/psd-tools/pull/657Release: https://github.com/psd-tools/psd-tools/releases/tag/v1.17.1Affected source (tag `v1.17.0`): `src/psd_tools/api/smart_object.py:170-179`
(sink), `:62-67` (untrusted `filename`); `src/psd_tools/psd/linked_layer.py:100`
(source).Distinct in class from the published advisories (GHSA-24p2-j2jr-386w —
compression resource exhaustion; GHSA-22jr-vc7j-g762 — buffer overflow). The
`save()` write logic is unchanged since the `SmartObject` API was introduced,
so all releases exposing it are affected.
CVSS v4.0 score: 4.6