RCE en el cargador ICO de imgproxy, corregido en 4.0.16 (GHSA-hff9-5pqw-6w9f)
imgproxy 4.0.0 a 4.0.15 asigna memoria para datos ICO con un tamaño que se desborda, y una imagen forjada corrompe el heap; corregido en 4.0.16.
Resumen
- El cargador de imágenes ICO de imgproxy 4.0.0 a 4.0.15 lee un campo de tamaño controlado por el atacante y le suma 14 en una suma de 32 bits que se desborda. Luego reserva muy poca memoria y escribe datos de imagen en ella: una ICO forjada hace caer al worker o corrompe el heap. El aviso del proveedor lo puntúa 9.8 Critical, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.
- Cualquiera que consiga que el proxy procese una imagen maliciosa puede alcanzar la falla. imgproxy ofrece transformaciones por HTTP y admite firmas de URL, desactivadas por defecto. mateofumis y JasonPuglisi divulgaron esta falla junto con dos problemas ICO relacionados.
- La corrección llegó en imgproxy 4.0.16 el 28 de septiembre de 2026, ocho días antes de los avisos del proveedor del 6 de octubre. GitHub los incorporó a su base de datos de avisos el 30 de octubre. La versión actual es 4.0.20; 4.0.16 y posteriores contienen la corrección.
- No se reporta exploit, explotación ni víctima, y el aviso no está en el catálogo KEV (Known Exploited Vulnerabilities) de CISA al 31 de octubre de 2026. Pase a 4.0.16 o posterior ahora; revise de paso cómo restringe las URL de origen.
¿Estoy afectado?
Todas estas condiciones deben cumplirse:
- Su instancia ejecuta imgproxy 4.0.0 a 4.0.15. Las versiones desde 4.0.16 están corregidas.
- Su instancia procesa imágenes controladas por atacantes, ya sea mediante URL de origen sin restricciones o mediante archivos subidos a un origen permitido.
- El cargador ICO está en uso, y lo está en las publicaciones afectadas. El cargador se registra para el formato ICO y el origen se decide según los bytes mágicos del archivo.
imgproxy admite URL firmadas. Exigir firmas y controlar qué imágenes firma su aplicación puede restringir el acceso al cargador; la autenticación en un proxy inverso también ayuda. Ninguna medida corrige el cargador ni vuelve seguras las imágenes maliciosas. Por separado, el aviso de asignación de memoria enumera como afectadas las versiones 3.3.0 a 4.0.15: quienes las ejecuten también deben actualizar a 4.0.16 o posterior.
El proveedor publicó la corrección el 28 de septiembre y sus avisos el 6 de octubre. La base de datos de avisos de GitHub publicó sus registros el 30 de octubre de 2026. Las herramientas que dependen de esa base pudieron conocer las fallas después de las que consultan directamente al proveedor.
| Rama | Versiones afectadas | Versión corregida |
|---|---|---|
| imgproxy 4.x | 4.0.0 a 4.0.15 | 4.0.16 (2026-09-28) |
Compruebe la versión que ejecuta. El binario imprime su propia versión:
docker exec imgproxy imgproxy --version
La etiqueta de la imagen también la lleva, pero las etiquetas flotantes como latest necesitan el comando de versión de esta sección. Si despliega desde el código, la constante de versión vive en version/version.go en la etiqueta de su copia.
¿Cómo lo corrijo?
Pase a 4.0.16 o posterior. La publicación actual es 4.0.20.
- En su archivo Compose, configure la imagen del servicio imgproxy como
darthsim/imgproxy:4.0.20. Después descargue esa imagen y recree el servicio:
docker compose pull imgproxy
docker compose up -d imgproxy
- Para un despliegue de binario o código, compile o descargue 4.0.16 o posterior y reinicie el servicio.
Restringir los orígenes reduce la exposición, pero no sustituye la actualización. IMGPROXY_ALLOWED_SOURCES acepta prefijos permitidos de URL de origen, por ejemplo:
IMGPROXY_ALLOWED_SOURCES="https://your-bucket.example/"
La lista de permitidos bloquea la descarga directa desde orígenes no autorizados. No detiene una imagen maliciosa subida a un bucket permitido ni es una corrección general para SSRF. Permita solo orígenes cuyo contenido considere fiable y actualice incluso con estas restricciones.
¿Cuál es la causa raíz?
El cargador ICO reserva un búfer a partir de un tamaño declarado por el atacante mediante una suma de 32 bits que puede desbordarse, y luego escribe a través de un puntero más allá de lo que realmente se asignó. El código vulnerable es vips_foreign_load_ico_header en vips/icoload.c, citado del aviso en el commit f4fedbb de la línea 4.0.15, líneas 154 a 164:
uint32_t data_size = GUINT32_FROM_LE(largest_image_header.data_size);
VipsPel *data = (VipsPel *) VIPS_MALLOC(NULL, data_size + BMP_FILE_HEADER_LEN);
void *actual_data = data + BMP_FILE_HEADER_LEN;
if (vips_foreign_load_read_full(ico->source, actual_data, data_size) <= 0) {
vips_error("vips_foreign_load_ico_header", "unable to read ICO image data from the source");
return -1;
}
Tres hechos en ese fragmento crean el defecto. data_size es un campo de 32 bits que el atacante controla mediante la entrada de directorio ICO. La suma data_size + BMP_FILE_HEADER_LEN, con la longitud de cabecera en 14, ocurre en aritmética de 32 bits: para data_size = 0xfffffff5 la suma se desborda a 3, así que la asignación es de tres bytes. vips_foreign_load_read_full recibe luego el data_size original, no el tamaño desbordado, y lo pasa al callback Go del origen, que copia los bytes disponibles del origen a través de actual_data a una dirección situada 14 bytes después del inicio de la asignación, ya fuera del búfer de tres bytes.
La aritmética del aviso para el ejemplo: una entrada de directorio con data_size = 0xfffffff5 y data_offset = 22, más una cabecera BMP de 14 bytes, da (0xfffffff5 + 14) mod 2^32 = 3. Los 64 bytes siguientes de la entrada se escriben antes de que la lectura pueda informar que la carga declarada de varios gigabytes está incompleta.
Las dos fallas compañeras están en la misma función. La reconstrucción de la cabecera BMP lee num_colors, bpp y height en desplazamientos fijos más allá de una asignación diminuta antes de cualquier comprobación de tamaño: una lectura fuera de límites que se convierte en escritura fuera de límites de la cabecera reconstruida. Y sin ninguna validación, una ICO de 23 bytes que declara data_size = 0xfa000068 hace que el worker intente una asignación de unos 4 GB, algo que el aviso de asignación no acotada dice haberse confirmado en un despliegue 4.0.15 en producción.
¿Cómo se explota?
No hay un exploit completo público. El aviso publica los valores forjados, y el compañero de asignación no acotada incluye un script generador de prueba de concepto cuya salida verifica con suma de control. Los pasos provienen del camino de origen a destino del aviso:
- El atacante aloja una ICO forjada. Su entrada de directorio declara el tamaño que desborda,
data_size = 0xfffffff5condata_offset = 22, y el archivo comienza con la magia ICO00 00 01 00. - El atacante hace que el imgproxy del objetivo procese ese archivo: una petición de su URL a través de una instancia con orígenes sin restricciones, o una subida directa donde un despliegue la acepta.
- imgproxy identifica el origen como ICO por la magia y lo despacha a
vips_icoload_source_go. vips_foreign_load_ico_headerlee eldata_sizedel atacante, asigna los tres bytes desbordados, yvips_foreign_load_read_fullpasa el tamaño declarado original al callback Go.- El callback escribe los bytes del origen de que dispone, corrompe el heap, y el proceso se interrumpe o continúa con un estado del asignador corrompido, lo que el aviso evalúa como denegación de servicio o ejecución remota de código.
¿Cómo fluye el ataque?
Una petición no autenticada que lleva una ICO forjada a través del proxy convierte un campo de tamaño que desborda en una escritura al heap antes de que ninguna comprobación pueda detenerla. La comprobación ausente es la validación de data_size antes de la asignación.
data_size 0xfffffff5"] --> B["La petición llega a imgproxy,
sin exigir firmas de URL"] B --> C["El origen despacha por la magia
00 00 01 00 al cargador ICO"] C --> D["comprobación ausente: data_size
nunca se valida"] D --> E["Desborde de 32 bits: asignación de
3 bytes para ~4 GB declarados"] E --> F["El callback de lectura escribe bytes
más allá de la asignación"] F --> G["El worker se interrumpe o el heap se corrompe:
caída, RCE potencial"]
¿Qué puede hacer un atacante?
El atacante hace caer al worker de imgproxy que procesa la imagen, y puede repetirlo a voluntad, lo cual es una denegación de servicio contra la canalización de imágenes. Más allá de la caída, el aviso señala que la corrupción del heap «may permit remote code execution», y su título dice que el desbordamiento de entero la «permite», puntuado con impacto completo en confidencialidad, integridad y disponibilidad a CVSS v3.1 9.8. Las condiciones son las de «¿Estoy afectado?»: una versión afectada, orígenes de imágenes influidos por el atacante y alcanzabilidad de red. No se publica ningún exploit que demuestre ejecución de código, así que el impacto demostrado es la caída, y la ejecución de código es la valoración del aviso.
Los compañeros añaden su propio alcance. La lectura-escritura fuera de límites de la reconstrucción de la cabecera BMP hace caer al worker de forma determinista, munmap_chunk(): invalid pointer de glibc en un pánico de Go, a CVSS v3.1 7.5. La asignación no acotada agota la memoria desde un archivo de 23 bytes, también 7.5. Ambos requieren que el atacante pueda enviar una imagen para su procesamiento.
No se reporta explotación en ningún lugar al 31 de octubre de 2026, y el aviso no está en el catálogo KEV.
¿Por qué funciona la corrección?
El commit a821b6a4cb07, «Fix segfaults during invalid ICO load», publicado en 4.0.16, valida el tamaño y calcula la suma una sola vez:
+// Maximum size of an ICO entry data block.
+// Maximum image size in ICO is 256x256 pixels,
+// which gives a maximum of 256 * 256 * 4 bytes for raw RGBA data.
+// 10 MB should be enough for any ICO entry data block,
+// even if an embedded PNG contains metadata.
+// Data blocks larger than this should be considered malicious.
+#define MAX_ICO_ENTRY_DATA_SIZE 10 * 1024 * 1024
+
/**
* ICO ForeignLoad VIPS class implementation (generic)
*/
@@ -153,13 +161,26 @@ vips_foreign_load_ico_header(VipsForeignLoad *load)
uint32_t data_size = GUINT32_FROM_LE(largest_image_header.data_size);
+ // Check if the data size exceeds the maximum allowed ICO entry data size.
+ if (data_size > MAX_ICO_ENTRY_DATA_SIZE) {
+ vips_error("vips_foreign_load_ico_header", "ICO image data is too large");
+ return -1;
+ }
+
// BMP file explicitly excludes BITMAPFILEHEADER, so we need to add it manually.
// We reserve space for it at the beginning of the data buffer.
- VipsPel *data = (VipsPel *) VIPS_MALLOC(NULL, data_size + BMP_FILE_HEADER_LEN);
+ uint32_t full_data_size = data_size + BMP_FILE_HEADER_LEN;
+ VipsPel *data = (VipsPel *) VIPS_MALLOC(NULL, full_data_size);
+ if (!data) {
+ vips_error("vips_foreign_load_ico_header", "unable to allocate memory for ICO image data");
+ return -1;
+ }
+
void *actual_data = data + BMP_FILE_HEADER_LEN;
if (vips_foreign_load_read_full(ico->source, actual_data, data_size) <= 0) {
vips_error("vips_foreign_load_ico_header", "unable to read ICO image data from the source");
+ VIPS_FREE(data);
return -1;
}
El tope de 10 MB elimina el desborde y la asignación no acotada en una sola comprobación. Un tamaño que desbordaría la suma de 32 bits está muy por encima del tope, y se rechaza con un error antes de cualquier asignación. La variable única full_data_size lleva un valor calculado una vez a cada uso posterior, así que el tamaño pasado aguas abajo ya no puede contradecir el tamaño asignado. La comprobación de nulidad tras VIPS_MALLOC gestiona una asignación fallida en lugar de desreferenciarla, y VIPS_FREE(data) en el camino de error detiene la fuga.
El mismo commit añade la comprobación de tamaño mínimo que cierra la lectura-escritura fuera de límites compañera:
+ // Ensure the data buffer is large enough to contain the BMP header.
+ if (full_data_size < BMP_BITMAP_INFO_HEADER_LEN) {
+ vips_error("vips_foreign_load_ico_header", "ICO image data is too small to contain a valid BMP header");
+ VIPS_FREE(data);
+ return -1;
+ }
+
// Read num_colors and bpp from the BITMAPINFOHEADER
uint32_t num_colors = GUINT32_FROM_LE(*(uint32_t *) (actual_data + 32));
La corrección es completa para el camino de entrada ICO de este cargador. El tamaño está acotado a la entrada, la asignación se comprueba, y la reconstrucción de la cabecera BMP ya no puede leer ni escribir más allá de un búfer más pequeño que una cabecera. La suite de pruebas del cargador ganó las ICO malformadas de los avisos, testdata/ico-bad1.ico a ico-bad3.ico, que no logran hacer caer al cargador corregido.
¿Cómo compruebo si estuve comprometido?
No hay indicadores de compromiso publicados, porque no se ha reportado ninguna víctima ni campaña. Busque la firma de caída que describen los avisos, que es el rastro observable de un intento:
# Worker restarts and ICO-related aborts in the container log.
docker logs imgproxy --since 720h 2>&1 | grep -iE "SIGABRT|panic|ico" | head -20
Un intento de corrupción del heap que hizo caer a un worker deja detrás la interrupción y un reinicio; una caída es una pista, no una prueba de ejecución de código. Si sospecha que el proceso ejecutó código del atacante, considere el contenedor comprometido y rote todo lo que pudiera leer, que para imgproxy incluye típicamente las credenciales de almacenamiento de objetos de su entorno. Revise sus registros de origen en busca de peticiones que llevaran archivos ICO desde fuera de los orígenes esperados.
Cronología
- 2026-09-24: imgproxy integra a821b6a4cb07, «Fix segfaults during invalid ICO load».
- 2026-09-28: imgproxy publica v4.0.16, cuya línea de registro dice «Fixed several vulnerabilities in ICO image loader».
- 2026-10-06: el proveedor publica los tres avisos ICO como avisos de seguridad de GitHub revisados, GHSA-hff9-5pqw-6w9f, GHSA-494m-4556-wf5p y GHSA-mmq5-xm89-9pfg.
- 2026-10-30: la revisión de la base de datos de avisos de GitHub publica los avisos a las 22:57 UTC, poniéndolos a disposición de las herramientas que consultan esa base.
Preguntas frecuentes
¿Por qué el aviso dice «may permit» para la ejecución remota de código?
El efecto demostrado es la caída, y la corrupción del heap que conduce a ejecución de código es la valoración del aviso de lo que el defecto permite. Esa valoración se refleja en su puntuación 9.8, con impacto completo en confidencialidad, integridad y disponibilidad. Ningún exploit público lleva la corrupción hasta la ejecución de código, así que el artículo conserva la cautela del propio aviso.
¿Basta 4.0.16, o necesito 4.0.20?
4.0.16 lleva las tres correcciones ICO, así que toda publicación desde ella es segura para estos avisos. 4.0.20 es simplemente la publicación actual, con actualizaciones de dependencias entremedias. Fije lo que pruebe, y fíjelo en 4.0.16 o posterior.
Mi escáner nunca marcó esto. ¿Por qué?
Los tres avisos no llevaban identificadores CVE, y no llegaron a la base de datos de avisos de GitHub hasta el 30 de octubre de 2026, un mes después de la corrección. Las herramientas que solo leen fuentes CVE o la base no vieron nada antes de esa fecha, así que un escaneo limpio de septiembre no significa una instancia limpia. Compruebe la versión, no el escaneo.
¿Restringir los orígenes realmente ayuda?
Restringir los orígenes bloquea la descarga directa desde orígenes no autorizados. No protege un servicio que procesa archivos del atacante dentro de un bucket permitido ni corrige el cargador. Actualice a 4.0.16 o posterior.
¿Y los otros dos avisos del mismo lote?
El lote también lleva un desbordamiento de búfer del heap en el cargador BMP personalizado, GHSA-55vq-g34m-xp8f, y una guarda de dirección de origen que permite direcciones CGNAT, GHSA-8w7m-q6hg-mfqm. Todos están corregidos en 4.0.16, así que una actualización cubre el lote. La falla del cargador BMP es la misma clase de caída no autenticada; la de la guarda es vecina del SSRF.
Inventarie sus canalizaciones de imágenes expuestas con Ostorlab
El descubrimiento de superficie de ataque de Ostorlab inventaría sus servicios expuestos en Internet. Muestra qué despliegues ejecutan un punto de acceso imgproxy expuesto, para saber qué instancias actualizar a 4.0.16 o posterior y cuáles limitar a orígenes de confianza.
Referencias
- Aviso del proveedor GHSA-hff9-5pqw-6w9f, «Integer overflow in ICO loader causes remote denial of service and permits remote code execution via heap buffer overflow», CVSS v3.1 9.8, afecta 4.0.0 a 4.0.15, corregido 4.0.16, acreditando a mateofumis y JasonPuglisi: https://github.com/imgproxy/imgproxy/security/advisories/GHSA-hff9-5pqw-6w9f (consultado el 31 de octubre de 2026)
- Aviso compañero GHSA-494m-4556-wf5p, «Out-of-bounds heap read/write in ICO loader BMP header reconstruction», CVSS v3.1 7.5: https://github.com/imgproxy/imgproxy/security/advisories/GHSA-494m-4556-wf5p (consultado el 31 de octubre de 2026)
- Aviso compañero GHSA-mmq5-xm89-9pfg, «Unbounded memory allocation in ICO loader», CVSS v3.1 7.5, con el generador de prueba de concepto verificado por suma de control y la confirmación en producción en 4.0.15: https://github.com/imgproxy/imgproxy/security/advisories/GHSA-mmq5-xm89-9pfg (consultado el 31 de octubre de 2026)
- Aviso compañero GHSA-55vq-g34m-xp8f, el desbordamiento de búfer del heap del cargador BMP personalizado: https://github.com/imgproxy/imgproxy/security/advisories/GHSA-55vq-g34m-xp8f (consultado el 31 de octubre de 2026)
- Aviso compañero GHSA-8w7m-q6hg-mfqm, la guarda de dirección de origen que permite direcciones CGNAT: https://github.com/imgproxy/imgproxy/security/advisories/GHSA-8w7m-q6hg-mfqm (consultado el 31 de octubre de 2026)
- Commit de corrección a821b6a4cb07, «Fix segfaults during invalid ICO load», escrito el 24 de septiembre de 2026: https://github.com/imgproxy/imgproxy/commit/a821b6a4cb07 (consultado el 31 de octubre de 2026)
- Publicación imgproxy v4.0.16, 28 de septiembre de 2026, línea de registro «Fixed several vulnerabilities in ICO image loader»: https://github.com/imgproxy/imgproxy/releases/tag/v4.0.16 (consultado el 31 de octubre de 2026)
- El
vips/icoload.cvulnerable en el commit f4fedbb citado por el aviso: https://github.com/imgproxy/imgproxy/blob/f4fedbb5c7f6684e7a8e0a9bb7e666773f3c40c0/vips/icoload.c (consultado el 31 de octubre de 2026) - Imagen Docker darthsim/imgproxy, 36,809,323 descargas: https://hub.docker.com/r/darthsim/imgproxy (consultado el 31 de octubre de 2026)
- Catálogo Known Exploited Vulnerabilities de CISA, fuente JSON, buscado para los avisos: https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (consultado el 31 de octubre de 2026)
- Firma de URL de imgproxy: https://docs.imgproxy.net/usage/signing_url