
Penpot's remote image import let an authenticated file editor turn a normal media convenience feature into backend-origin SSRF because attacker-controlled URLs crossed into a redirect-following server fetch path without destination filtering.
La importación remota de imágenes de Penpot permitía que un editor de archivos autenticado convirtiera una función de medios normal en SSRF de origen de backend porque las URL controladas por el atacante cruzaban hacia una ruta de búsqueda del servidor que seguía redirecciones sin filtrar destinos.
Encontré este problema al revisar Penpot, la plataforma de diseño y colaboración de código abierto, con una pregunta muy específica en mente:
¿Qué sucede cuando una herramienta de diseño colaborativo permite que un usuario entregue al backend una URL de imagen remota para obtenerla?
En este caso, esa pregunta condujo a un error real.
El flujo de importación de imágenes remotas de Penpot aceptaba una URL controlada por el usuario y hacía que el backend la obtuviera desde el contexto de la red del servidor sin imponer restricciones de destino para objetivos de loopback o red privada. El cliente HTTP compartido también seguía las redirecciones automáticamente.
Eso convirtió una función de medios normal en una primitiva SSRF autenticada de origen de backend y finalmente se convirtió en CVE-2026-45806.
Penpot: Penpot en GitHub
CVE: CVE-2026-45806
CVSS: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Esto afectó a Penpot. En su sitio web oficial y kit de medios, Penpot se presenta con una base de usuarios en crecimiento de +1M y dice que decenas de miles de organizaciones lo usan, incluyendo Blender, Mozilla, Fedora, NTT Data, , , , , y .

editor de archivos autenticado -> URL de imagen remota controlada por el atacante -> create-file-media-object-from-url -> descarga de imagen del backend con redirecciones habilitadas -> solicitud final llega a un endpoint interno solo de imágenes -> SSRF de origen de backend / alcance interno
Penpot es una plataforma de diseño y colaboración de código abierto.
Maneja cosas como:
Eso significa que su ruta de importación de medios se encuentra en un límite de confianza real.
La pregunta importante aquí no era si Penpot admite la importación de imágenes remotas.
La verdadera pregunta era:
¿Restringe Penpot a dónde puede conectarse el backend cuando un usuario importa una imagen remota?
En este caso, no lo hacía.
Mucha gente subestima las funciones de importación remota.
Eso es un error.
En el momento en que una aplicación:
crea un límite de confianza de salida real.
Ese era el problema aquí.
Este error no estaba en el renderizado de imágenes. No estaba en el almacenamiento de archivos. No estaba en las comprobaciones de permisos habituales para editar un archivo.
Era una falla de confianza del lado del servidor clásica:
Eso es suficiente para crear una vulnerabilidad real.
No abordé Penpot lanzando RPC aleatorios de forma ciega o buscando caídas primero.
El enfoque más sólido fue identificar el límite de seguridad más prometedor.
Para Penpot, ese era la importación de medios remotos.
¿Por qué?
Porque esta función combina:
Ese era el límite correcto para inspeccionar.
Y fue exactamente donde vivía el error.
El error se reduce a una pequeña cadena de confianza.
En el frontend:
(defn upload-media-url
[name file-id url]
(rp/cmd!
:create-file-media-object-from-url
{:name name
:file-id file-id
:url url
:is-local true}))
la URL url controlada por el usuario se envía directamente a la llamada RPC.
Luego, en el backend:
(sv/defmethod ::create-file-media-object-from-url
...
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(files/check-edition-permissions! pool profile-id file-id)
...
(let [_ (files/get-minimal-file cfg file-id)
mobj (create-file-media-object-from-url cfg (assoc params :profile-id profile-id))])
y:
(defn- create-file-media-object-from-url
[cfg {:keys [url name] :as params}]
(let [content (media/download-image cfg url)
el backend verifica que el llamante pueda editar el archivo de destino, luego pasa la URL controlada por el atacante a media/download-image.
La implementación de la búsqueda está aquí:
(defn download-image
"Download an image from the provided URI and return the media input object"
[{:keys [::http/client]} uri]
...
(http/req! client
{:method :get :uri uri}
{:response-type :input-stream})
Y el cliente HTTP compartido está configurado como:
(http/build-client {:connect-timeout 30000
:follow-redirects :always}))
Esa es toda la vulnerabilidad:
Porque el atacante solo necesita:
La cadena de ataque es sencilla:
Ese es todo el error.
La distinción importante es dónde ocurre la solicitud.
La pregunta no es:
"¿Puede Penpot importar imágenes desde URL?"
La verdadera pregunta es:
"¿Puede un usuario autenticado hacer que el backend de Penpot se conecte a destinos internos a los que el usuario no debería poder acceder a través de la aplicación?"
En este caso, la respuesta fue sí.
Eso importa porque hay una diferencia real entre:
La validación de imágenes no elimina esa diferencia.
Reduce algunos casos de exfiltración directa, pero no elimina la condición SSRF ni la ruptura del límite de red.
Validé este problema con una prueba local controlada vinculada directamente a la ruta de código revisada de Penpot.
El objetivo no era alcanzar infraestructura de terceros. El objetivo era demostrar la propiedad de seguridad exacta:
Construí un validador Java autocontenido que reflejaba el comportamiento relevante:
content-type y content-lengthValidé dos casos.
El validador solicitó:
http://127.0.0.1:7790/internal.png
Resultado observado:
http://127.0.0.1:7790/internal.pnghttp://127.0.0.1:7790/internal.png200image/pngEso demostró que la lógica de búsqueda estilo importación aceptaba un endpoint de imágenes solo interno directamente.
El validador luego solicitó:
http://localhost:7791/redirect-to-internal
Ese endpoint devolvió una redirección HTTP a:
http://127.0.0.1:7790/internal.png
Resultado observado:
http://localhost:7791/redirect-to-internalhttp://127.0.0.1:7790/internal.png200image/pngEl listener solo interno registró la solicitud redirigida.
Eso demostró la afirmación más importante:
El payload aquí fue intencionalmente simple:
Eso importaba porque Penpot no solo obtiene bytes arbitrarios y se detiene. Realiza validación orientada a medios después de la solicitud.
Por lo tanto, la prueba correcta no era:
"el backend puede intentar conectarse a algún lugar"
La prueba más sólida era:
"se puede hacer que el backend se conecte a algún lugar interno y complete la solicitud con éxito bajo las mismas restricciones similares a imágenes que la función espera"
Eso es exactamente lo que demostró la validación.
Una reacción común a errores SSRF como este es:
"el objetivo aún debe devolver una imagen"
Esa observación es cierta pero incompleta.
No elimina la vulnerabilidad.
Solo te dice qué objetivos internos son más directamente útiles.
Este problema aún permite:
Eso sigue siendo una ruptura real del límite de seguridad.
Especialmente en entornos autoalojados, los servicios internos a menudo existen específicamente detrás de ese límite.
Este problema finalmente recibió una gravedad Alta en CVSS:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Esa clasificación tiene sentido.
La afirmación no es que un atacante no autenticado pueda comprometer instantáneamente cada implementación de Penpot desde cero.
La afirmación es que cualquier editor de archivos autenticado normal puede convertir Penpot en una primitiva de solicitud de backend contra destinos internos, incluyendo acceso asistido por redirección a objetivos de loopback y red privada.
Hubo cierta discusión sobre la gravedad durante la divulgación, principalmente en torno a:
Esas son restricciones justas para discutir.
Pero no eliminan el problema central:
Eso es una vulnerabilidad SSRF real y defendible.
La corrección importante aquí no es un manejo MIME más estricto.
La verdadera corrección es una política de destino saliente.
Una remediación correcta para esta clase de error necesita:
http y httpslocalhostEsa es la dirección correcta de corrección porque esto no era un error de análisis de imágenes. Era un error de límite de confianza de red.
Este problema se reportó de forma privada a través del flujo de reporte de seguridad de GitHub.
El informe incluía:
Los mantenedores confirmaron el problema y comenzaron a trabajar en una resolución.
El problema luego recibió:
CVE-2026-45806
La lección clave aquí es simple:
la importación remota de medios es un límite de confianza de salida, no solo una función de conveniencia
Muchos desarrolladores piensan en términos de:
Esos son detalles de implementación.
La verdadera pregunta de seguridad es:
¿a dónde puede conectarse el backend en nombre de un usuario?
Si esa pregunta no se responde explícitamente, funciones como la importación remota se convierten en superficies SSRF por defecto.
Este error también refuerza algo importante sobre la revisión de SSRF:
Esa es la verdadera conclusión.
Esta vulnerabilidad no se trataba de un payload llamativo.
Se trataba de hacer la pregunta correcta sobre el límite de confianza.
Penpot permitió que un editor de archivos autenticado proporcionara una URL de imagen remota, y el backend confió en esa URL más de lo que debería. El manejo de redirecciones hizo el resto.
Por eso esto se convirtió en CVE-2026-45806.