Pillow FlashPix : RCE par dépassement de tampon sur le tas (CVE-2026-112398)
Le décodage d’une image FlashPix spécialement conçue dans Pillow 12.3.0 ou antérieur, avec olefile installé, permet à un attaquant d’exécuter des commandes ; Pillow 13.0.0 corrige CVE-2026-112398.
En bref
CVE-2026-112398 est un dépassement de tampon sur le tas critique (CVSS v4.0 9.2) dans le décodeur JPEG FlashPix de Pillow. Il permet d’exécuter des commandes choisies par l’attaquant lorsqu’une application décode une image non fiable, dans toutes les versions jusqu’à 12.3.0 avec olefile installé.
- Le plugin FlashPix de Pillow décode des tuiles JPEG dans un conteneur OLE2, le format Compound File Binary de Microsoft. Il dimensionne le tampon de décodage à partir du mode couleur annoncé par les métadonnées FlashPix, puis laisse libjpeg produire des lignes selon le nombre de composantes propre au JPEG intégré. Un fichier qui annonce 2 octets par pixel alors que son JPEG intégré en produit 4 dépasse le tampon de décodage à chaque ligne.
- Tout service qui ouvre et décode des fichiers envoyés non fiables est exposé s’il utilise Pillow jusqu’à 12.3.0 avec le package facultatif olefile installé : le fichier est détecté par sa signature de contenu, pas par son extension, et le dépassement se déclenche dans un simple appel
Image.open().load()sans interaction utilisateur. - L’éditeur attribue CWE-122 (dépassement de tampon sur le tas) et CWE-787 (écriture hors limites), et son bulletin inclut une preuve de concept qui exécute une commande sur Pillow 12.2.0 standard. L’enrichissement Authorized Data Publisher (ADP) de CISA indique « poc » comme preuve d’exploitation. La faille ne figure pas dans le catalogue KEV et aucune attaque réelle n’est connue au 24 octobre 2026.
- Mettez à jour vers Pillow 13.0.0, publié le 16 octobre 2026. Si ce n’est pas possible, désinstallez olefile, ce qui désactive la prise en charge de FlashPix et Microsoft Image Composer, ou rejetez les fichiers OLE2 avant qu’ils n’atteignent Pillow.
Suis-je concerné ?
Vous êtes concerné si toutes ces conditions sont réunies :
- Votre application, ou un composant qu’elle importe, ouvre des fichiers non fiables avec
Image.open()puis force leur décodage avec.load(),.thumbnail(),.tobytes()ou tout autre appel qui lit les données des pixels. - La version installée de Pillow est 12.3.0 ou antérieure. Toutes les versions précédentes sont affectées ; la fiche CVE indique la plage antérieure à 13.0.0.
- Le package facultatif olefile est installé dans cet environnement. La détection FlashPix l’importe, et le packaging actuel de Pillow le déclare comme dépendance facultative pour les fonctionnalités
fpxetmic.
Vous n’êtes pas concerné si le traitement des images utilise Pillow 13.0.0 ou ultérieur, ou si olefile est absent, auquel cas le plugin FlashPix n’est jamais chargé.
PyPI a enregistré 141 847 434 téléchargements de pillow la semaine dernière (vérification le 24 octobre 2026). Pillow arrive souvent comme dépendance transitive : auditez donc chaque environnement, pas seulement les projets qui le déclarent directement.
| Branche | Versions affectées | Version corrigée |
|---|---|---|
| 13.x | aucune | 13.0.0 et ultérieures |
| 12.x et toutes les précédentes | <= 12.3.0 | 13.0.0 |
Vérifiez les versions exécutées par vos environnements, y compris les images de conteneurs et les environnements de build :
pip show pillow | grep -E "^(Name|Version)"
python -c "import PIL; print(PIL.__version__)"
python -c "import olefile; print('olefile', olefile.__version__)" 2>/dev/null || echo "olefile absent"
# pip requirements files
grep -i "^pillow" requirements*.txt
# Pipenv and pyproject.toml
grep -i "pillow" Pipfile.lock pyproject.toml 2>/dev/null
# Poetry
grep -A2 "name = \"pillow\"" poetry.lock
# Container images
docker run --rm <image> python -c "import PIL; print(PIL.__version__)"
Comment corriger le problème ?
Mettez à jour vers la version corrigée :
pip install --upgrade "pillow>=13.0.0"
Pillow 13.0.0 a abandonné la prise en charge de Python 3.10 : les environnements figés sur 3.10 nécessitent donc une mise à jour de Python ou la solution de contournement ci-dessous.
Si vous ne pouvez pas encore effectuer la mise à jour, retirez la dépendance qui rend la faille accessible. La prise en charge de FlashPix nécessite olefile : sa désinstallation désactive donc le chemin vulnérable.
pip uninstall -y olefile
Cela supprime la prise en charge de FlashPix et de Microsoft Image Composer (MIC), les formats que le bulletin identifie comme dépendant d’olefile. La limite est claire : un service qui traite légitimement des fichiers FlashPix ou MIC perd cette capacité, et tout autre code de l’environnement qui importe olefile cesse également de fonctionner.
Une passerelle peut à la place rejeter les fichiers envoyés qui commencent par la signature OLE2 D0CF11E0A1B11AE1, celle que le plugin vérifie lui-même. Cela bloque FlashPix à l’entrée, quelle que soit l’extension du fichier.
Quelle est la cause racine ?
Le plugin FlashPix dimensionne son tampon de décodage JPEG à partir des métadonnées FlashPix, tandis que libjpeg écrit des lignes dimensionnées d’après l’en-tête du JPEG intégré. Aucune vérification ne comparait ces deux tailles.
Le plugin FlashPix associe les identifiants de couleur du conteneur aux modes Pillow dans src/PIL/FpxImagePlugin.py à la version 12.2.0, lignes 24 à 33 :
MODES = {
# opacity
(0x00007FFE,): ("A", "L"),
# monochrome
(0x00010000,): ("L", "L"),
(0x00018000, 0x00017FFE): ("RGBA", "LA"),
...
(0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"),
}
L’attaquant choisit quelle ligne du tableau s’applique en inscrivant ces identifiants de couleur dans le fichier. La ligne ("RGBA", "LA") sélectionne le rawmode LA, soit 2 octets par pixel. Le décodeur alloue le tampon de ligne à partir de ce rawmode, dans src/decode.c à la version 12.2.0, lignes 223 à 226 :
state->bytes = (state->bits * state->xsize + 7) / 8; // "LA" gives 2 * xsize bytes per row
...
state->buffer = (UINT8 *)calloc(1, state->bytes);
Le décodeur JPEG associe ensuite la chaîne rawmode à un espace colorimétrique de sortie libjpeg, dans src/libImaging/JpegDecode.c à la version 12.2.0. Les rawmodes reconnus, tels que CMYK, YCbCr et YCbCrK, reçoivent un espace colorimétrique précis. LA et YCC;P ne sont pas reconnus : l’exécution atteint donc le cas de repli aux lignes 219 à 221 :
/* Disable decoder conversions. */
context->cinfo.jpeg_color_space = JCS_UNKNOWN;
context->cinfo.out_color_space = JCS_UNKNOWN;
Avec JCS_UNKNOWN, libjpeg n’effectue aucune conversion de couleur et produit num_components octets par pixel, une valeur que l’attaquant inscrit dans le marqueur SOF du JPEG intégré. S’il la fixe à 4 alors que le tampon est dimensionné pour 2, chaque appel à jpeg_read_scanlines à la ligne 252 écrit une ligne à 4 composantes dans un tampon prévu pour 2 octets par pixel :
ok = jpeg_read_scanlines(&context->cinfo, &state->buffer, 1); // writes 4 * width into a 2 * width buffer
La longueur du dépassement est contrôlée par l’attaquant via la largeur de l’image. Les octets qui débordent sont les pixels décodés, que l’attaquant façonne au travers du flux JPEG plutôt que de les écrire directement. La vérification de signature qui rend ce chemin accessible se trouve au début du même fichier de plugin :
def _accept(prefix: bytes) -> bool:
return prefix.startswith(olefile.MAGIC)
Il s’agit de la signature OLE2, recherchée dans le contenu de chaque fichier que Pillow doit ouvrir.
Comment la faille est-elle exploitée ?
Un attaquant construit un fichier FlashPix dont les métadonnées annoncent 2 octets par pixel, mais dont le JPEG intégré déclare 4 composantes. Son décodage dépasse le tampon sur le tas, et la PoC du bulletin transforme ce dépassement en détournement d’un pointeur de type qui appelle system().
Le chemin d’attaque, tel que démontré par la preuve de concept du bulletin :
- L’attaquant construit un fichier FlashPix autonome sur sa propre machine avec le générateur de payload du bulletin. Cette construction ne nécessite aucun accès à la cible.
- Le fichier annonce le mode
("RGBA", "LA"), si bien que Pillow dimensionne le tampon de tuile à 2 octets par pixel, et intègre un JPEG dont le marqueur SOF déclare 4 composantes : libjpeg produit donc 4 octets par pixel. - Le service victime reçoit le fichier par un chemin d’envoi ou de récupération quelconque et force le décodage vulnérable. L’opération complète de la PoC est
Image.open(path).load():
#!/usr/bin/env python3
# The entire vulnerable operation an application performs on an untrusted file.
import sys
from PIL import Image
Image.open(sys.argv[1]).load()
- La signature OLE2 dans l’en-tête du fichier sélectionne le plugin FlashPix, quel que soit son nom. Le dépassement se produit dès le premier
load(). - Le dépassement écrase un objet
bytesadjacent au tampon de décodage, en implantant un faux objet de type et une vtable dans la même mémoire du tas contrôlée. Lorsque Pillow libère le tampon d’entrée,PyBuffer_Releasesuit le pointeur de type détourné et effectue un appel à travers celui-ci. La PoC dirige cet appel verssystem@pltavec la chaîne de commande dans le registre du premier argument, etsystem("id")s’exécute sous l’identité de l’utilisateur chargé du traitement d’images.
La PoC publiée cible une image Docker figée avec ASLR désactivé afin que ses adresses prédéfinies restent valides. id s’exécute, puis l’interpréteur s’arrête sur le tas corrompu avec corrupted size vs. prev_size, après l’exécution de la commande. Sur un serveur normal, le même dépassement doit gérer la randomisation des adresses : la preuve de concept démontre donc le bug et la voie vers son exploitation, plutôt qu’un exploit universel en une seule tentative.
Comment le dépassement FlashPix se déclenche-t-il ?
En une passe, la signature OLE2 sélectionne le plugin FlashPix, le rawmode LA dimensionne le tampon de ligne à 2 octets par pixel, JCS_UNKNOWN fait produire à libjpeg les 4 composantes JPEG, et la ligne surdimensionnée déborde sur l’objet suivant du tas.
envoyé au service"] --> B["Image.open() reconnaît
la signature OLE2 D0CF11E0"] B --> C["Les identifiants couleur FlashPix sélectionnent
rawmode LA, 2 octets par pixel"] C --> D["Tampon de décodage alloué
à 2 * largeur octets"] D --> E["rawmode LA non reconnu,
libjpeg utilise JCS_UNKNOWN"] E --> F["Le JPEG intégré déclare 4
composantes, lignes écrites
à 4 octets par pixel"] F --> G["Écriture hors limites sur le tas
avec le contenu de l’attaquant"] G --> H["Pointeur de type de
l’objet adjacent écrasé"] H --> I["Le hook de libération appelle
la cible de l’attaquant, le code s’exécute"]
Que peut faire un attaquant ?
Le décodage du fichier spécialement conçu exécute des commandes choisies par l’attaquant sous l’identité de l’utilisateur chargé du traitement d’images. Cela signifie la compromission complète de ce processus, de ses données et de ses privilèges. Le bulletin décrit l’impact comme une exécution de commandes arbitraires dans le contexte de ce processus. Les conditions sont les suivantes :
- Un accès réseau à un endpoint qui ouvre des fichiers influencés par l’attaquant et force leur décodage (
.load(),.thumbnail(),.tobytes()), avec la dépendance facultative olefile installée. Le bulletin cite les services d’envoi et de miniatures, le traitement d’avatars et de médias, et les pipelines de conversion de documents et d’images. - Aucune interaction utilisateur ni astuce d’extension : la détection repose sur le contenu. Un fichier nommé
avatar.pngcontenant des octets OLE2 emprunte donc le chemin FlashPix. - L’exécution en une tentative de la PoC est démontrée sur une image figée avec ASLR désactivé. Une cible réelle avec randomisation des adresses nécessite donc un travail d’exploitation supplémentaire, tandis que l’écriture hors limites sous-jacente se produit sur toutes les versions affectées.
Pourquoi le correctif fonctionne-t-il ?
Le correctif ajoute une vérification de bornes dans ImagingJpegDecode avant la lecture de toute ligne, en rejetant les flux dont la largeur de sortie ou la taille de ligne dépasse le tampon de décodage. Le commit du correctif, fusionné sous #10131, ajoute cette vérification :
+ /* The scanline buffer was sized from the tile extents and the
+ unpacker, not from the JPEG header, so make sure the lines
+ libjpeg is about to produce actually fit in it. */
+ if (context->cinfo.output_width > (JDIMENSION)state->xsize ||
+ (size_t)context->cinfo.output_components * context->cinfo.output_width >
+ (size_t)state->bytes) {
+ jpeg_destroy_decompress(&context->cinfo);
+ state->errcode = IMAGING_CODEC_BROKEN;
+ return -1;
+ }
La vérification rejette un JPEG dont la largeur dépasse celle de la tuile ou dont les composantes dépassent la taille par pixel du tampon. Elle détruit le décompresseur et signale « broken data stream » au lieu de décoder. Elle élimine cette classe d’incohérences là où la taille du tampon et l’en-tête JPEG se rencontrent enfin : non dans la table FlashPix, qui associe toujours les modes LA, mais dans le décodeur, qui refuse désormais d’écrire des lignes trop grandes.
C’est un correctif complet pour les entrées JPEG dont les lignes sont surdimensionnées. Il ne supprime pas le chemin JCS_UNKNOWN lui-même, qui reste correct tant que les lignes tiennent dans le tampon.
Comment vérifier si j’ai été compromis ?
Aucun indicateur de compromission, domaine ou infrastructure d’attaquant n’est publié pour ce problème. Le dépassement laisse trois types de traces à rechercher dans votre environnement :
- Des journaux de plantage de workers de traitement d’images avec des messages de corruption du tas, comme l’arrêt
corrupted size vs. prev_sizelaissé par la PoC, ou des arrêts SIGSEGV de processus qui ouvrent des images envoyées. - Des fichiers envoyés dont les 8 premiers octets sont la signature OLE2
D0 CF 11 E0 A1 B1 1A E1, mais dont le nom ou le type déclaré n’est pas FlashPix. C’est la signature de livraison décrite par la vérification de signature propre au plugin. - Des tâches de traitement qui ont ouvert une image puis exécuté des commandes ou lancé des processus inattendus, puisque l’exécution se produit dans le contexte du worker lui-même.
Chronologie
- 2026-10-06 : Andrew Murray (radarhere) enregistre la vérification de bornes des lignes, fusionnée sous #10131.
- 2026-10-16 : Pillow publie 13.0.0 avec le correctif.
- 2026-10-17 : Pillow publie le bulletin GHSA-5x8w-8jj5-q52h, classé critique, CVSS v4.0 9.2, avec une preuve de concept complète, en créditant Alex Bouffard (pebwalker).
- 2026-10-19 : la CNA GitHub publie la fiche CVE-2026-112398.
- 2026-10-22 : le bloc d’enrichissement Authorized Data Publisher (ADP) de CISA enregistre « poc » comme preuve d’exploitation.
- 2026-10-23 : la GitHub Advisory Database publie et examine le bulletin.
- 2026-10-24 : publication de cet article.
FAQ
Mon service doit-il avoir olefile installé pour être concerné ?
Oui. Le plugin FlashPix importe olefile, et le packaging de Pillow le déclare comme dépendance facultative pour les extras fpx et mic. Sans lui, le plugin ne se charge jamais et le fichier OLE2 est rejeté avant le décodage. Les environnements qui ne l’ont jamais installé n’empruntent pas le chemin vulnérable.
Renommer ou bloquer les fichiers .fpx envoyés suffit-il ?
Non. Image.open() sélectionne le plugin FlashPix d’après les octets de signature OLE2 au début du fichier. L’extension n’a donc aucune incidence sur l’attaque ni sur la défense. Rejeter les fichiers selon le préfixe de signature D0CF11E0A1B11AE1 à la frontière d’envoi les bloque bien, car il s’agit de la même signature que celle exigée par le plugin.
La PoC publiée est-elle un exploit distant complet ?
C’est une démonstration fonctionnelle d’exécution de commandes, mais elle cible une image Docker figée avec ASLR désactivé pour que les adresses prédéfinies du payload soient fixes. Sur un serveur avec randomisation des adresses, le même dépassement nécessite un travail supplémentaire pour placer des adresses ou les divulguer. Le dépassement lui-même et le contrôle du tas qu’il donne à l’attaquant se comportent de manière identique sur toutes les versions affectées.
Les fichiers JPEG ordinaires sont-ils également dangereux ?
Un JPEG ordinaire n’emprunte pas ce chemin. Le bug nécessite que le conteneur FlashPix sélectionne le rawmode LA et le repli JCS_UNKNOWN, ce que l’ouverture d’un JPEG ordinaire ne fait jamais. Le correctif rejette aussi désormais tout JPEG dont les lignes dépassent le tampon : cette classe générale est donc traitée au niveau du décodeur.
Quels autres problèmes Pillow 13.0.0 a-t-il corrigés ?
La même version a également corrigé CVE-2026-112900, une injection d’arguments dans l’appel à Ghostscript pour les entrées EPS, qui affecte toutes les versions de Pillow depuis 2.6.0 et que le projet a classée critique, CVSS v4.0 9.2. La mise à jour vers 13.0.0 couvre les deux problèmes à la fois.
Examinez vos services de traitement d’images
Lancez une évaluation de sécurité avec Ostorlab, puis utilisez les vérifications de dépendances ci-dessus pour confirmer la version de Pillow et la présence d’olefile dans chaque environnement de traitement d’images. Mettez les déploiements affectés à jour vers Pillow 13.0.0 ou ultérieur.
Références
- Bulletin Pillow GHSA-5x8w-8jj5-q52h avec la preuve de concept et le laboratoire Docker, publié le 17 octobre 2026 : https://github.com/python-pillow/Pillow/security/advisories/GHSA-5x8w-8jj5-q52h (consulté le 24 octobre 2026)
- Fiche CVE-2026-112398 avec le vecteur CVSS v4.0 9.2 et l’enrichissement « poc » de CISA ADP : https://cveawg.mitre.org/api/cve/CVE-2026-112398 (consultée le 24 octobre 2026)
- Entrée de la GitHub Advisory Database : https://github.com/advisories/GHSA-5x8w-8jj5-q52h (consultée le 24 octobre 2026)
- Commit du correctif 242a059a, « Reject JPEG streams whose scanlines are wider or have more components », daté du 6 octobre 2026 : https://github.com/python-pillow/Pillow/commit/242a059aedc1935097d9aa1dc3f099750467ba75 (consulté le 24 octobre 2026)
- Version Pillow 13.0.0, publiée le 16 octobre 2026 : https://github.com/python-pillow/Pillow/releases/tag/13.0.0 (consultée le 24 octobre 2026)
- Code vulnérable de FpxImagePlugin à la version 12.2.0 : https://raw.githubusercontent.com/python-pillow/Pillow/12.2.0/src/PIL/FpxImagePlugin.py (consulté le 24 octobre 2026)
- Code vulnérable de JpegDecode.c à la version 12.2.0 : https://raw.githubusercontent.com/python-pillow/Pillow/12.2.0/src/libImaging/JpegDecode.c (consulté le 24 octobre 2026)
- pyproject.toml de Pillow déclarant olefile comme extra facultatif pour fpx et mic : https://raw.githubusercontent.com/python-pillow/Pillow/12.2.0/pyproject.toml (consulté le 24 octobre 2026)
- Statistiques de téléchargement PyPI pour pillow, 141 847 434 téléchargements la semaine dernière : https://pypistats.org/packages/pillow (consultées le 24 octobre 2026)
- Bulletin associé pour l’injection d’arguments Ghostscript corrigée dans la même version : https://github.com/python-pillow/Pillow/security/advisories/GHSA-w889-685w-c8fh (consulté le 24 octobre 2026)