Neutron, notre moteur d’IA, a obtenu un score de 97,1 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Analyse approfondie

Analyse approfondie

RCE du chargeur ICO d'imgproxy, corrigé dans 4.0.16 (GHSA-hff9-5pqw-6w9f)

imgproxy 4.0.0 à 4.0.15 alloue les données ICO avec une taille qui boucle, transformable en corruption du tas par une image forgée ; corrigé dans 4.0.16.

TL;DR

  • Le chargeur d'images ICO d'imgproxy 4.0.0 à 4.0.15 lit un champ de taille contrôlé par l'attaquant et y ajoute 14 dans une somme 32 bits qui boucle. Il alloue alors bien trop peu de mémoire et y écrit des données d'image : une ICO forgée fait planter le worker ou corrompt le tas. L'avis du fournisseur le note 9,8 Critical, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.
  • Toute personne capable de faire traiter une image malveillante par le proxy peut atteindre la faille. imgproxy sert les transformations sur HTTP et prend en charge la signature des URL, désactivée par défaut. mateofumis et JasonPuglisi ont divulgué cette faille avec deux autres problèmes ICO.
  • Le correctif est paru dans imgproxy 4.0.16 le 28 septembre 2026, huit jours avant les avis du fournisseur du 6 octobre. GitHub les a ajoutés à sa base d'avis le 30 octobre. La version actuelle est 4.0.20 ; les versions 4.0.16 et ultérieures contiennent le correctif.
  • Aucun exploit, aucune exploitation et aucune victime ne sont signalés, et l'avis n'est pas dans le catalogue KEV (Known Exploited Vulnerabilities) de la CISA au 31 octobre 2026. Passez à 4.0.16 ou une version ultérieure maintenant ; vérifiez au passage comment vous restreignez les URL sources.

Suis-je concerné ?

Toutes ces conditions doivent être réunies :

  • Vous exécutez imgproxy 4.0.0 à 4.0.15. Les versions à partir de 4.0.16 sont corrigées.
  • Votre instance traite des images contrôlées par un attaquant, via des URL sources non restreintes ou des fichiers déposés dans une source autorisée.
  • Le chargeur ICO est en service, ce qui est le cas dans les publications concernées. Le chargeur s'enregistre pour le format ICO et la source répartit selon les octets magiques du fichier.

imgproxy prend en charge les URL signées. Imposer les signatures et contrôler les images que votre application signe peut restreindre l'accès au chargeur ; une authentification sur le proxy inverse peut aussi aider. Ces protections ne corrigent pas le chargeur et ne rendent pas les images malveillantes inoffensives. L'avis compagnon sur l'allocation mémoire indique séparément que les versions 3.3.0 à 4.0.15 sont concernées : leurs utilisateurs doivent aussi passer à 4.0.16 ou une version ultérieure.

Le fournisseur a publié le correctif le 28 septembre et ses avis le 6 octobre. La base d'avis GitHub a publié ses fiches le 30 octobre 2026. Les outils qui dépendent de cette base ont donc pu être informés après ceux qui surveillent directement le fournisseur.

Chronologie de la divulgation du chargeur ICO : correctifs committés le 24 septembre, v4.0.16 publiée le 28 septembre, avis du fournisseur publiés le 6 octobre, relecture de la base le 30 octobre
Figure 1 : le correctif est paru huit jours avant les avis du fournisseur. Source : publications et avis de sécurité imgproxy, consultés le 31 octobre 2026.

Branche Versions concernées Version corrigée
imgproxy 4.x 4.0.0 à 4.0.15 4.0.16 (2026-09-28)

Vérifiez la version exécutée. Le binaire affiche sa propre version :

docker exec imgproxy imgproxy --version

Le tag d'image la porte aussi, mais les tags flottants comme latest demandent la commande de version de cette section. En déploiement depuis les sources, la constante de version vit dans version/version.go au tag de votre extraction.

Comment corriger ?

Passez à 4.0.16 ou plus. La publication courante est 4.0.20.

  1. Dans votre fichier Compose, définissez l'image du service imgproxy sur darthsim/imgproxy:4.0.20, puis téléchargez cette image et recréez le service :
docker compose pull imgproxy
docker compose up -d imgproxy
  1. Pour un déploiement binaire ou source, compilez ou téléchargez 4.0.16 ou plus et redémarrez le service.

Restreindre les sources réduit l'exposition, mais ne remplace pas la mise à jour. IMGPROXY_ALLOWED_SOURCES accepte les préfixes d'URL sources autorisés, par exemple :

IMGPROXY_ALLOWED_SOURCES="https://your-bucket.example/"

La liste d'autorisation bloque les téléchargements directs depuis les sources non autorisées. Elle n'arrête pas une image malveillante déposée dans un bucket autorisé et ne constitue pas un correctif général contre les SSRF. N'autorisez que des sources dont vous maîtrisez le contenu et mettez à jour même avec ces restrictions.

Quelle est la cause racine ?

Le chargeur ICO alloue un tampon à partir d'une taille déclarée par l'attaquant avec une addition 32 bits qui peut boucler, puis écrit à travers un pointeur au-delà de ce qui a réellement été alloué. Le code vulnérable est vips_foreign_load_ico_header dans vips/icoload.c, cité depuis l'avis au commit f4fedbb de la lignée 4.0.15, lignes 154 à 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;
}

Trois faits dans cet extrait font le bug. data_size est un champ 32 bits que l'attaquant contrôle via l'entrée de répertoire ICO. L'addition data_size + BMP_FILE_HEADER_LEN, avec une longueur d'en-tête de 14, se fait en arithmétique 32 bits : pour data_size = 0xfffffff5, la somme boucle à 3, donc l'allocation fait trois octets. vips_foreign_load_read_full reçoit ensuite le data_size original, pas la taille bouclée, et le passe au callback source Go, qui copie les octets disponibles dans la source à travers actual_data vers une adresse située 14 octets après le début de l'allocation, déjà hors du tampon de trois octets.

L'arithmétique de l'avis pour l'exemple : une entrée de répertoire avec data_size = 0xfffffff5 et data_offset = 22, plus un en-tête BMP de 14 octets, donne (0xfffffff5 + 14) mod 2^32 = 3. Les 64 octets qui suivent l'entrée sont écrits avant que la lecture puisse signaler que la charge déclarée de plusieurs gigaoctets est incomplète.

Les deux failles compagnes sont dans la même fonction. La reconstruction d'en-tête BMP lit num_colors, bpp et height à décalages fixes au-delà d'une minuscule allocation avant toute vérification de taille : une lecture hors bornes qui devient une écriture hors bornes de l'en-tête reconstruit. Et sans aucune validation, une ICO de 23 octets déclarant data_size = 0xfa000068 fait tenter au worker une allocation d'environ 4 Go, ce que l'avis d'allocation non bornée dit avoir été confirmé sur un déploiement 4.0.15 en production.

Comment la faille est-elle exploitée ?

Aucun exploit complet n'est public. L'avis publie les valeurs forgées, et le compagnon d'allocation non bornée inclut un script générateur de preuve de concept dont il vérifie la somme de contrôle. Les étapes viennent du chemin source-vers-puits de l'avis :

  1. L'attaquant héberge une ICO forgée. Son entrée de répertoire déclare la taille qui boucle, data_size = 0xfffffff5 avec data_offset = 22, et le fichier commence par la magie ICO 00 00 01 00.
  2. L'attaquant fait traiter ce fichier par l'imgproxy de la cible : une requête de son URL via une instance aux sources non restreintes, ou un téléversement direct là où un déploiement l'accepte.
  3. imgproxy identifie la source comme ICO à partir de la magie et la répartit vers vips_icoload_source_go.
  4. vips_foreign_load_ico_header lit le data_size de l'attaquant, alloue les trois octets bouclés, et vips_foreign_load_read_full passe la taille déclarée originale au callback Go.
  5. Le callback écrit les octets sources dont il dispose, corrompt le tas, et le processus s'interrompt ou continue avec un allocateur corrompu, ce que l'avis évalue comme déni de service ou exécution de code à distance.

Quel est le flux de l'attaque ?

Une requête non authentifiée qui achemine une ICO forgée à travers le proxy transforme un champ de taille qui boucle en écriture dans le tas avant qu'aucune vérification ne puisse l'arrêter. La vérification manquante est la validation de data_size avant l'allocation.

flowchart LR A["L'attaquant héberge une ICO forgée
data_size 0xfffffff5"] --> B["La requête atteint imgproxy,
sans imposer de signature des URL"] B --> C["La source répartit sur la magie
00 00 01 00 vers le chargeur ICO"] C --> D["vérification manquante : data_size
n'est jamais validé"] D --> E["bouclage 32 bits : allocation de
3 octets pour ~4 Go déclarés"] E --> F["Le callback de lecture écrit les octets
au-delà de l'allocation"] F --> G["Le worker s'interrompt ou le tas est corrompu :
plantage, RCE potentielle"]

Que peut faire un attaquant ?

L'attaquant fait planter le worker imgproxy qui traite l'image, et peut le répéter à volonté, ce qui est un déni de service contre le pipeline d'images. Au-delà du plantage, l'avis indique que la corruption du tas « may permit remote code execution », et son titre dit que le débordement d'entier la « permet », notée avec impact complet en confidentialité, intégrité et disponibilité à CVSS v3.1 9,8. Les conditions sont celles de « Suis-je concerné ? » : une version concernée, des sources d'images influencées par l'attaquant, et une accessibilité réseau. Aucun exploit ne démontrant l'exécution de code n'est publié, donc l'impact démontré est le plantage, et l'exécution de code est l'évaluation de l'avis.

Les compagnons ajoutent leur propre portée. La lecture-écriture hors bornes de la reconstruction d'en-tête BMP fait planter le worker de façon déterministe, munmap_chunk(): invalid pointer de glibc en panique Go, à CVSS v3.1 7,5. L'allocation non bornée épuise la mémoire depuis un fichier de 23 octets, aussi 7,5. Toutes deux nécessitent que l'attaquant puisse soumettre une image au traitement.

Aucune exploitation n'est signalée nulle part au 31 octobre 2026, et l'avis n'est pas dans le catalogue KEV.

Pourquoi le correctif fonctionne-t-il ?

Le commit a821b6a4cb07, « Fix segfaults during invalid ICO load », publié dans 4.0.16, valide la taille et calcule la somme une seule fois :

+// 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;
   }

Le plafond de 10 Mo supprime le bouclage et l'allocation non bornée en une seule vérification. Une taille qui ferait boucler la somme 32 bits dépasse largement le plafond, et elle est rejetée par une erreur avant toute allocation. La variable unique full_data_size porte une valeur calculée une fois vers chaque usage ultérieur, donc la taille passée en aval ne peut plus contredire la taille allouée. La vérification de nullité après VIPS_MALLOC gère une allocation échouée au lieu de la déréférencer, et VIPS_FREE(data) sur le chemin d'erreur arrête la fuite.

Le même commit ajoute la vérification de taille minimale qui ferme la lecture-écriture hors bornes compagne :

+    // 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));

Le correctif est complet pour le chemin d'entrée ICO de ce chargeur. La taille est bornée à l'entrée, l'allocation est vérifiée, et la reconstruction d'en-tête BMP ne peut plus lire ni écrire au-delà d'un tampon plus petit qu'un en-tête. La suite de tests du chargeur a gagné les ICO malformées des avis, testdata/ico-bad1.ico à ico-bad3.ico, qui ne font plus planter le chargeur corrigé.

Comment vérifier si j'ai été compromis ?

Aucun indicateur de compromission n'est publié, car aucune victime ni campagne n'a été signalée. Cherchez la signature de plantage que les avis décrivent, qui est la trace observable d'une tentative :

# Worker restarts and ICO-related aborts in the container log.
docker logs imgproxy --since 720h 2>&1 | grep -iE "SIGABRT|panic|ico" | head -20

Une tentative de corruption du tas qui a fait planter un worker laisse derrière elle l'interruption et un redémarrage ; un plantage est une piste, pas une preuve d'exécution de code. Si vous soupçonnez le processus d'avoir exécuté du code attaquant, considérez le conteneur comme compromis et révoquez tout ce qu'il pouvait lire, ce qui pour imgproxy inclut typiquement les identifiants de stockage d'objets de son environnement. Passez en revue vos journaux de sources pour les requêtes transportant des fichiers ICO venant d'origines inattendues.

Chronologie

  • 2026-09-24 : imgproxy commite a821b6a4cb07, « Fix segfaults during invalid ICO load ».
  • 2026-09-28 : imgproxy publie v4.0.16, dont la ligne de journal dit « Fixed several vulnerabilities in ICO image loader ».
  • 2026-10-06 : le fournisseur publie les trois avis ICO comme avis de sécurité GitHub relus, GHSA-hff9-5pqw-6w9f, GHSA-494m-4556-wf5p et GHSA-mmq5-xm89-9pfg.
  • 2026-10-30 : la relecture de la base de données d'avis GitHub publie les avis à 22 h 57 UTC, les rendant accessibles aux outils qui consultent cette base.

FAQ

Pourquoi l'avis dit-il « may permit » pour l'exécution de code à distance ?

L'effet démontré est le plantage, et la corruption du tas menant à l'exécution de code est l'évaluation par l'avis de ce que le bug permet. Cette évaluation se reflète dans son score 9,8, avec impact complet en confidentialité, intégrité et disponibilité. Aucun exploit public ne porte la corruption jusqu'à l'exécution de code, donc l'article garde la prudence de l'avis.

4.0.16 suffit-il, ou faut-il 4.0.20 ?

4.0.16 porte les trois correctifs ICO, donc toute publication depuis celle-ci est sûre pour ces avis. 4.0.20 est simplement la publication courante, avec des mises à jour de dépendances entre-temps. Épinglez ce que vous testez, et épinglez-le à 4.0.16 ou plus.

Mon scanner ne m'a jamais signalé cela. Pourquoi ?

Les trois avis ne portaient pas d'identifiant CVE, et ils n'ont atteint la base de données d'avis GitHub que le 30 octobre 2026, un mois après le correctif. Les outils qui ne lisent que les flux CVE ou la base n'ont rien vu avant cette date, donc un scan propre de septembre ne signifie pas une instance propre. Vérifiez la version, pas le scan.

Restreindre les sources aide-t-il vraiment ?

Restreindre les sources bloque les téléchargements directs depuis les sources non autorisées. Cela ne protège pas un service qui traite les fichiers d'un attaquant dans un bucket autorisé et ne corrige pas le chargeur. Passez à 4.0.16 ou une version ultérieure.

Que dire des deux autres avis du même lot ?

Le lot porte aussi un débordement de tampon du tas dans le chargeur BMP personnalisé, GHSA-55vq-g34m-xp8f, et une garde d'adresse source qui autorise les adresses de traduction d'adresses de niveau opérateur, GHSA-8w7m-q6hg-mfqm. Tous sont corrigés dans 4.0.16, donc une mise à jour couvre le lot. La faille du chargeur BMP est la même classe de plantage non authentifié ; la faille de garde est voisine du SSRF.

Recensez vos pipelines d'images exposés avec Ostorlab

La découverte de surface d'attaque d'Ostorlab recense vos services exposés sur Internet. Elle montre quels déploiements exécutent un point de terminaison imgproxy exposé, pour savoir quelles instances passer à 4.0.16 ou une version ultérieure et lesquelles verrouiller sur des sources de confiance.

Références

  • Avis du fournisseur 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, versions 4.0.0 à 4.0.15, corrigé 4.0.16, créditant mateofumis et JasonPuglisi : https://github.com/imgproxy/imgproxy/security/advisories/GHSA-hff9-5pqw-6w9f (consulté le 31 octobre 2026)
  • Avis compagnon 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 (consulté le 31 octobre 2026)
  • Avis compagnon GHSA-mmq5-xm89-9pfg, « Unbounded memory allocation in ICO loader », CVSS v3.1 7,5, avec le générateur de preuve de concept vérifié par somme de contrôle et la confirmation en production sur 4.0.15 : https://github.com/imgproxy/imgproxy/security/advisories/GHSA-mmq5-xm89-9pfg (consulté le 31 octobre 2026)
  • Avis compagnon GHSA-55vq-g34m-xp8f, le débordement de tampon du tas du chargeur BMP personnalisé : https://github.com/imgproxy/imgproxy/security/advisories/GHSA-55vq-g34m-xp8f (consulté le 31 octobre 2026)
  • Avis compagnon GHSA-8w7m-q6hg-mfqm, la garde d'adresse source autorisant les adresses CGNAT : https://github.com/imgproxy/imgproxy/security/advisories/GHSA-8w7m-q6hg-mfqm (consulté le 31 octobre 2026)
  • Commit correctif a821b6a4cb07, « Fix segfaults during invalid ICO load », écrit le 24 septembre 2026 : https://github.com/imgproxy/imgproxy/commit/a821b6a4cb07 (consulté le 31 octobre 2026)
  • Publication imgproxy v4.0.16, 28 septembre 2026, ligne de journal « Fixed several vulnerabilities in ICO image loader » : https://github.com/imgproxy/imgproxy/releases/tag/v4.0.16 (consulté le 31 octobre 2026)
  • Le vips/icoload.c vulnérable au commit f4fedbb cité par l'avis : https://github.com/imgproxy/imgproxy/blob/f4fedbb5c7f6684e7a8e0a9bb7e666773f3c40c0/vips/icoload.c (consulté le 31 octobre 2026)
  • Image Docker darthsim/imgproxy, 36 809 323 téléchargements : https://hub.docker.com/r/darthsim/imgproxy (consulté le 31 octobre 2026)
  • Catalogue Known Exploited Vulnerabilities de la CISA, flux JSON, recherché pour les avis : https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (consulté le 31 octobre 2026)
  • Signature des URL imgproxy: https://docs.imgproxy.net/usage/signing_url