XXE/SSRF/XSS almacenados mediante evasión del sanitizador SVG
GHSA-9c7h-xjrp-8gh3 · Asignación de CVE pendiente (GitHub CNA)
Resumen
Ambas rutas de subida — el POST /api/v1/upload independiente y el de biblioteca POST /api/v1/files/upload — ejecutaban el sanitizador SVG solo después de que el validador de imagen hubiera decodificado el archivo:
// apps/api/src/routes/files.ts:117 (misma forma en user-files.ts:337)
const safeBuffer = isValidImage && isSvgBuffer(buffer)
? sanitizeSvg(buffer)
: buffer;
Como las rutas aceptan cualquier tipo de archivo, un SVG que librsvg (vía Sharp) no puede analizar se almacena de todos modos: un DOCTYPE que declara una entidad externa hace fallar el analizador, isValidImage es false, y el cortocircuito && omite sanitizeSvg() completamente. El archivo crudo se sirve luego como image/svg+xml con su DOCTYPE, declaraciones de entidad y cualquier marcado ejecutable intacto.
Dos debilidades relacionadas dentro del propio sanitizador:
- Eliminaba solo elementos peligrosos sin prefijo, así que un elemento con prefijo de espacio de nombres
sobrevivía incluso en SVGs que el decodificador aceptaba. - Su regex de eliminación de DOCTYPE fallaba cuando un
]aparecía dentro de un valor de entidad entre comillas.
Impacto
- XSS almacenado — un usuario autenticado que puede subir archivos almacena un SVG con marcado activo; un espectador que abre el SVG almacenado directamente en un navegador (o cualquier superficie que lo renderice inline) ejecuta el script incrustado.
- Recuperación sin autenticación — la URL de descarga de subida escalonada
/api/v1/download/:jobId/:filenameno requiere autenticación, así que cualquiera con el job id puede obtener el archivo. - Vector de ataque: red — subida como usuario autenticado, luego interacción de la víctima.
- Los vectores encadenados de lectura de archivos y falsificación de peticiones del informe original no se reprodujeron en la aplicación publicada: librsvg se niega a resolver la entidad externa (esa negativa es exactamente por qué falla la validación), las rutas de herramientas rechazan SVGs inválidos antes de procesarlos, las descargas llevan
Content-Disposition: attachment+X-Content-Type-Options: nosniff, y el CSP de respuesta no permite script inline — así que un SVG almacenado no se ejecuta como documento servido desde el origen de la app.
Causa Raíz
La sanitización estaba acoplada a la validación de imagen exitosa en lugar de ejecutarse en toda subida con SVG. La corrección invierte la responsabilidad: ambas rutas de subida ahora sanitizan el contenido SVG independientemente de si el decodificador de imagen acepta el archivo, el sanitizador elimina elementos peligrosos con prefijo de espacio de nombres, y la detección SVG más eliminación de DOCTYPE manejan un prólogo y valores entre comillas.
Prueba de Concepto
SVG creado cuyo DOCTYPE hace fallar el decodificador, evadiendo el sanitizador:
]>
La reproducción usó cuatro cargas — xss-event.svg (ejecución onload), xss-cookie.svg (exfiltración de cookies), xxe-file-read.svg y ssrf-metadata.svg (los dos vectores descartados posteriormente por el análisis de impacto del aviso) — subidas con una sesión autenticada, luego recuperadas vía la URL de descarga sin autenticación y abiertas en un navegador para confirmar la ejecución de script.
Demostraciones de Prueba de Concepto
Recorrido completo del exploit (1:41)
Explotación de extremo a extremo: crear carga maliciosa URL, disparar vía API, demostrar ejecución de JavaScript en contexto de víctima, exfiltrar token de sesión, lograr toma de cuenta.
Ejecución mínima de carga XSS (7 s)
Clip corto que muestra la carga XSS básica javascript:alert(1) ejecutándose en el contexto de respuesta — confirma el punto de reflexión sin la cadena completa de explotación.
Videos: evasión del sanitizador SVG de SnapOtter (GHSA-9c7h-xjrp-8gh3). Demos completas en la página de PoC Demos.
Cronología de Divulgación
| Fecha | Evento |
|---|---|
| 2026-09-11 | Vulnerabilidad descubierta durante auditoría autorizada |
| 2026-09-12 | PoC y videos demo creados; informe enviado a contact@snapotter.com |
| 2026-09-14 | Sin acuse de recibo tras 48h — seguimiento enviado |
| 2026-09-19 | Mantenedor confirmó la vulnerabilidad, la clasificó High, crédito a xaferima |
| 2026-09-20 | Degradación a HIGH aceptada (librsvg bloquea XXE); CVE a solicitar vía GitHub CNA |
| 2026-11-03 | Aviso publicado — GHSA-9c7h-xjrp-8gh3 — y corregido en SnapOtter v2.3.0 |
| Pendiente | Asignación de CVE vía GitHub CNA |
Corrección
Sanitizar subidas SVG incondicionalmente — independiente del validador de imagen — eliminar elementos peligrosos con prefijo de espacio de nombres, y reforzar la detección SVG y eliminación de DOCTYPE:
// Ambas rutas de subida, tras la corrección:
const safeBuffer = isSvgBuffer(buffer) ? sanitizeSvg(buffer) : buffer;
Corregido en SnapOtter v2.3.0 (2026-11-03, también el lanzamiento de agradecimiento del millón de pulls del proyecto, que acredita este informe en las notas de versión). Aviso publicado 2026-11-03; asignación de CVE pendiente.