Threat Overview
Pillow disclosed CVE-2026-112900 on 17 October 2026, with renewed technical reporting on 23 October. Pillow versions 2.6.0 through 12.x pass a caller-influenced EPS filename to Ghostscript after -f without normalizing the path. Filenames beginning with – may be interpreted as Ghostscript switches; command execution requires a leading @ argument-file filename and attacker control of the referenced argument file. The vulnerability was fixed in Pillow 13.0.0 released 16 October. A proof of concept exists; no verified in-the-wild exploitation was established in reviewed reporting. This is a filename/argument-injection issue, not proof that every crafted EPS image triggers RCE.
References
Pillow GHSA-w889-685w-c8fh, 2026-10-17; updated 2026-10-21
The Hacker News, Pillow image library code execution report, 2026-10-23
Pillow 13.0.0 release, 2026-10-16
Impacted Systems
Vendor/product: Python Pillow PIL.EpsImagePlugin.Ghostscript() with Ghostscript installed. Affected versions: Pillow >=2.6.0 and <13.0.0. Fixed: Pillow 13.0.0 or later (validate downstream distribution backports). Platform/deployment: Linux or Windows Python application servers, image conversion services, upload/thumbnail/document pipelines and containerized workloads that process EPS via Ghostscript. Prerequisites: attacker influence over the filename passed to Pillow; RCE requires an @-prefixed argument-file path and attacker control of the referenced text file. Exposure: externally reachable upload/conversion service or other untrusted filename entry point; no special TCP port intrinsic to Pillow. Unaffected by this specific path: workloads without Ghostscript/EPS processing, and workflows that do not pass attacker-influenced filenames to this API.
Why this matters
Pillow is a widely used transitive Python dependency; application teams may not recognize that EPS processing spawns an external interpreter. The observable Python→Ghostscript→shell chain can provide high-fidelity post-exploitation evidence.
Exploitation Status
Maintainer-confirmed: vulnerable code path, affected version range, fix 13.0.0. Researcher-confirmed: reproducible argument injection and command execution when strict prerequisites are met. Not confirmed: active exploitation against production organizations or published attack infrastructure.
What this hunt looks for
This package defines searches for Sentinel DeviceProcessEvents, DeviceNetworkEvents, DeviceFileEvents, optional CommonSecurityLog with upload filename/URL visibility, and Windows SecurityEvent 4688 fallback. Exact application logs or EPS file contents are not assumed ingested. No customer logs were reviewed or queried during preparation; the sections below describe what would be reviewed where the relevant data source is ingested into Microsoft Sentinel.
Required logs
DeviceProcessEvents; DeviceProcessEvents + DeviceNetworkEvents; DeviceFileEvents; SecurityEvent; CommonSecurityLog
Hunt 01 — Ghostscript Spawns a Shell
Signal: High. Telemetry: DeviceProcessEvents. Strong post-exploitation indicator: gs or gswin child launching shell/command interpreter.
DeviceProcessEvents
| where TimeGenerated >= ago(14d)
| where InitiatingProcessFileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe","gswin32.exe","gswin64.exe")
| where FileName in~ ("sh","bash","dash","zsh","cmd.exe","powershell.exe","pwsh.exe","python","python3","python.exe")
| project TimeGenerated,DeviceName,FileName,ProcessCommandLine,InitiatingProcessFileName,InitiatingProcessCommandLine,InitiatingProcessAccountName
| order by TimeGenerated desc
Hunt 02 — Python-Launched Ghostscript with Argument File
Signal: High. Telemetry: DeviceProcessEvents. Pillow prerequisite pattern: Python parent and Ghostscript command-line argument-file token.
DeviceProcessEvents
| where TimeGenerated >= ago(14d)
| where FileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe","gswin32.exe","gswin64.exe")
| where InitiatingProcessFileName in~ ("python","python3","python.exe","python3.exe","pythonw.exe")
| where ProcessCommandLine contains "@" and ProcessCommandLine contains " -f"
| project TimeGenerated,DeviceName,FileName,ProcessCommandLine,InitiatingProcessFileName,InitiatingProcessCommandLine,AccountName
| order by TimeGenerated desc
Hunt 03 — Ghostscript Unexpected Option Arguments
Signal: Follow-up. Telemetry: DeviceProcessEvents. Follow-up on Ghostscript called by Python with options that could reflect filename injection; tune to legitimate workflows.
DeviceProcessEvents
| where TimeGenerated >= ago(14d)
| where FileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe")
| where InitiatingProcessFileName in~ ("python","python3","python.exe","python3.exe")
| where ProcessCommandLine has_any ("-sDEVICE=","-sOutputFile=","-dNOSAFER","-sPermitFileReading=","-sPermitFileWriting=")
| project TimeGenerated,DeviceName,ProcessCommandLine,InitiatingProcessCommandLine,AccountName
| order by TimeGenerated desc
Hunt 04 — Ghostscript Process Makes Public Network Connection
Signal: High. Telemetry: DeviceProcessEvents + DeviceNetworkEvents. Correlate exact Ghostscript process ID and creation time with outbound traffic.
let GS=DeviceProcessEvents
| where TimeGenerated >= ago(14d)
| where FileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe")
| project DeviceId,DeviceName,GSProcessId=ProcessId,GSStart=ProcessCreationTime,GSCommand=ProcessCommandLine,Parent=InitiatingProcessFileName;
DeviceNetworkEvents
| where TimeGenerated >= ago(14d) and RemoteIPType =~ "Public"
| join kind=inner GS on DeviceId,$left.InitiatingProcessId==$right.GSProcessId
| where InitiatingProcessCreationTime==GSStart
| project TimeGenerated,DeviceName,Parent,GSCommand,RemoteIP,RemotePort,RemoteUrl,ActionType
| order by TimeGenerated desc
Hunt 05 — Python-to-Ghostscript First Seen on Host
Signal: Follow-up. Telemetry: DeviceProcessEvents. Baseline discovery: first Python-spawned Ghostscript on a device in last 3 days.
let Old=DeviceProcessEvents
| where TimeGenerated between (ago(30d)..ago(3d))
| where FileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe")
| where InitiatingProcessFileName in~ ("python","python3","python.exe","python3.exe")
| summarize by DeviceId;
DeviceProcessEvents
| where TimeGenerated >= ago(3d)
| where FileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe")
| where InitiatingProcessFileName in~ ("python","python3","python.exe","python3.exe")
| join kind=leftanti Old on DeviceId
| project TimeGenerated,DeviceName,FileName,ProcessCommandLine,InitiatingProcessFileName,InitiatingProcessCommandLine
| order by TimeGenerated desc
Hunt 06 — Suspicious EPS Filename Creation
Signal: Follow-up. Telemetry: DeviceFileEvents. Creation of EPS or argument-file artifacts with option-like names; correlate to known upload directories.
DeviceFileEvents
| where TimeGenerated >= ago(14d)
| where ActionType in~ ("FileCreated","FileRenamed")
| where FileName startswith "@" or (FileName startswith "-" and FileName endswith ".eps")
| project TimeGenerated,DeviceName,ActionType,FileName,FolderPath,InitiatingProcessFileName,InitiatingProcessCommandLine
| order by TimeGenerated desc
Hunt 07 — Windows Native 4688 Python-to-Ghostscript
Signal: Follow-up. Telemetry: SecurityEvent. Fallback when Defender process table absent and SecurityEvent 4688 command lines are available.
SecurityEvent
| where TimeGenerated >= ago(14d) and EventID==4688
| where NewProcessName has_any ("gswin32c.exe","gswin64c.exe","gs.exe")
| where ParentProcessName has_any ("python.exe","pythonw.exe")
| project TimeGenerated,Computer,Account,NewProcessName,ParentProcessName,CommandLine
| order by TimeGenerated desc
Hunt 08 — HTTP Uploads with Option-Like EPS Filenames
Signal: Follow-up. Telemetry: CommonSecurityLog. Only when URL/request logging preserves upload filenames; not a generic EPS signature.
CommonSecurityLog
| where TimeGenerated >= ago(14d)
| extend Url=tostring(column_ifexists("RequestURL","")),Ext=tostring(column_ifexists("AdditionalExtensions","")),Msg=tostring(column_ifexists("Message",""))
| where Url has ".eps" or Ext has ".eps" or Msg has ".eps"
| where Url contains "%40" or Url contains "@" or Ext contains "filename=\"@" or Msg contains "filename=\"@"
| project TimeGenerated,SourceIP,DestinationIP,Url,DeviceVendor,DeviceProduct,DeviceAction,Ext,Msg
| order by TimeGenerated desc
Detection Notes
Highest signal: Ghostscript spawning a shell, and Python→Ghostscript with @ argument-file semantics or unexpected Ghostscript options. Normal image conversion creates Python→Ghostscript, so that ancestry alone is a baseline lead. The literal @ can be absent if telemetry truncates command lines. File creation queries are weak without verified upload directories. WAF logs rarely capture multipart filename parameters; absent matches cannot exclude exploitation. A crafted filename is necessary, but the image body itself need not be malicious.