Mutation XSS en el editor Trix (bypass sanitizer↔serializer)
Escondes un onerror en data-trix-serialized-attributes que sobrevive a DOMPurify; cuando el editor-jefe abre el borrador, Trix re-materializa el handler y exfiltras la publishing key en su sesión.
Aprende a encontrar este bug
Un reporte real de HackerOne, reproducido para que lo practiques.
Crea tu cuenta y practica bugs reales que se pagaron. Descarga el entorno, encuéntralo y aprende la técnica exacta — tu camino a tu primer bounty.
hunters entrenando
labs de reportes reales
completaciones
en bounties practicados
Acceso a todos los labs · sin permanencia · cancela cuando quieras
Hunters que lo han resuelto· 3
Objetivos
Logro que recibirás
Cuando resuelvas este lab desbloqueas este logro compartible
Mutation XSS en el editor Trix (bypass sanitizer↔serializer)
Writeups de la comunidad
Contenido del lab
Escenario
Redactia es un CMS editorial SaaS para medios y newsletters. Los colaboradores (contributor) redactan artículos en Redactia Studio, un composer que monta un editor de texto enriquecido. Cuando un colaborador termina un borrador lo "envía a revisión", y un editor-jefe (editor, rol administrador) lo abre en una vista de revisión que re-hidrata el contenido en un editor real para anotarlo y aprobarlo antes de publicar.
Eres pentester con ámbito sobre el entorno de staging. Tu rol es el de un colaborador de bajo privilegio: lo que escribes acaba, tarde o temprano, delante de alguien con más permisos que tú. La organización guarda una clave de publicación que solo un editor puede consultar; tú, como colaborador, recibes 403 si la pides.
Tu misión: demostrar que el flujo de revisión permite ejecutar código en la sesión del editor-jefe y exfiltrar esa clave de publicación (la flag).
Qué tienes
- Una cuenta de colaborador de demostración:
attacker@redactia.local/password123— es tu punto de partida, no es en sí la vulnerabilidad. - El composer de Redactia Studio para redactar y enviar borradores a revisión.
- Una vista pública de artículos ya publicados (
/read/:slug) y una bandeja de estados que refleja cuándo alguien abre tu borrador.
Guía paso a paso
- Recon del editor. Entra como colaborador y abre "Nuevo borrador". El composer no es un
<textarea>: es un editor de texto enriquecido de una librería open-source conocida. Identifica cuál y qué versión exacta carga (inspecciona el asset servido y lo que la librería expone en el navegador, p.ej. su objeto de configuración). Reconocer una versión con un fallo publicado es el primer signpost, igual que reconocer un CVE. - Refuta el decoy obvio. Prueba lo evidente: mete un
<script>o un<img>con un manejador de evento plano en un borrador y guárdalo. Vuelve a leerlo (o pideGET /api/drafts/:id) y observa qué queda: un sanitizador borra lo evidente. Conclusión con una sola prueba: el filtro básico funciona, necesitas otro canal. - Encuentra lo que sobrevive. Vuelve a mirar el contenido guardado con atención. No todo se borra: los atributos que empiezan por
data-trix-*sobreviven intactos porque el editor los necesita para reconstruir el documento. Adjunta una imagen en el composer y estudia la forma exacta que genera el editor: ahí hay un canal que el sanitizador considera inofensivo. - Localiza el molde de anidación. No todos los adjuntos del editor son imágenes. Algunos borradores ya sembrados llevan adjuntos cuyo
contentes HTML incrustado (text/html: tarjetas, "datos clave") que el editor renderiza al reconstruir el documento. Fíjate en esos borradores: esecontentes un lienzo de HTML que sobrevive al sanitizador dentro del adjunto. Es tu plantilla de anidación. - Construye el payload. El editor guarda ciertos atributos "en reserva" dentro de una estructura serializada (
data-trix-serialized-attributes) y, al reconstruir el documento, los vuelve a aplicar uno por uno, a ciegas víasetAttribute. Si dentro de esa reserva colocas un manejador de evento (unonerror) en lugar de un atributo benigno, el sanitizador ve un data-attribute inocuo, pero al re-hidratar el editor lo materializa como handler vivo. Coloca esa reserva en el HTML de un adjuntotext/htmlque el editor re-renderiza en la vista de revisión. - Entrega y confirma la ejecución. Guarda el borrador (por el composer o
POST /api/drafts) y envíalo a revisión (POST /api/drafts/:id/submit). Empieza con un beacon inofensivo: haz que tu payload avise aPOST /collectcuando se ejecute, y léelo enGET /collect/log. Ese es tu canal para saber que el editor-jefe abrió el borrador y tu código corrió en su sesión. - Cosecha el impacto. Ya tienes ejecución en la sesión del editor-jefe. Su cookie es
httpOnly, así quedocument.cookieno te da el token: no robes la cookie, usa la sesión. Hay un recurso reservado al rol editor (una "clave de publicación") que a ti te devuelve403. Haz que tu handler lo pida con unfetchsame-origin —el navegador adjunta la cookie del editor automáticamente— y reenvíe la respuesta a tu sink en/collect. La flag vive dentro de esa clave.
Canales de feedback (no vuelas a ciegas)
- El estado del borrador (borrador → enviado → revisado) te dice que alguien lo abre después de ti.
- El sanitizador es observable: comparas lo que envías con lo que devuelve
GET /api/drafts/:idy ves exactamente qué atributos se conservan y cuáles se borran. POST /collect+GET /collect/loges tu sink out-of-band: confirma con un beacon inofensivo que tu contenido ejecuta código cuando el editor-jefe lo revisa, antes de armar el exfil real.
Decoys (descártalos por razonamiento acotado)
- "Trix + DOMPurify ya sanea": es el decoy central y es honesto. El sanitizador elimina de verdad
<script>,javascript:y loson*sueltos. Pero descartarlo requiere una sola prueba: losdata-trix-*sobreviven. El defecto no es que el sanitizador falle, sino que el editor materializa lo que el sanitizador aprobó. - La vista pública
/read/:slug: renderiza HTML endurecido sin editor real, sin re-hidratar Trix. Ahí tu payload nunca despierta — es un callejón sin salida verificable. El vector es únicamente elcontentrico que pasa por el editor en la vista de revisión. - El campo de notas/comentarios: es texto plano escapado; no ejecuta nada.
- Robar la cookie: la cookie del editor-jefe es
httpOnly.document.cookieno la lee. El camino no es exfiltrar la cookie, sino aprovechar la sesión con unfetchsame-origin.
La falla del laboratorio
Clase: Mutation XSS por discrepancia sanitizer↔serializer (CWE-79, Cross-site Scripting) con codificación/escape de salida incorrectos (CWE-116), que encadena a una autorización incorrecta del recurso admin-only (CWE-863) y a la restricción impropia de una capa de UI renderizada (CWE-1021). OWASP A03:2021 — Injection (XSS).
El fallo no está en que el sanitizador sea débil: DOMPurify hace bien su trabajo con <script>, javascript: y los on* sueltos. El problema es que el sanitizador y el motor de render no coinciden en qué es "seguro":
- El sanitizado server-side conserva a propósito los atributos
data-trix-*(el documento del editor los necesita para re-hidratarse), así que a ojos de DOMPurify undata-trix-serialized-attributeses un data-attribute benigno y sobrevive al filtro. - El serializador del editor, al reconstruir el documento, lee ese
data-trix-serialized-attributesy re-aplica sus pares a ciegas víael.setAttribute(name, value), sin re-sanear. Unonerrorque era texto inerte en un data-attribute nace vivo como manejador de evento. - El handler solo se materializa en el render-through-editor de la vista de revisión (no en un
innerHTMLdirecto, ni en la vista pública endurecida): por eso el payload es inerte hasta que el editor-jefe abre el borrador en su editor real, y entonces elonerrorse ejecuta same-origin en su sesión autenticada. - Como la cookie del editor-jefe es
httpOnly, el XSS no roba la cookie: usa la sesión. Unfetchsame-origin al recurso editor-only (que al colaborador le da403) lee la clave de publicación con la sesión de la víctima y la exfiltra out-of-band.
La librería de edición implicada es Trix, un editor de texto enriquecido open-source real (Basecamp); el lab reproduce de forma anonimizada —empresa ficticia Redactia— un fallo conocido de su serializador presente en la versión 2.1.16 y corregido en 2.1.17 (que sanea recursivamente el serializado antes de re-aplicarlo). Nombrar Trix está permitido: es una librería pública y load-bearing para la fidelidad de la técnica; ningún objetivo, ID de reporte ni investigador real aparece en el lab.
Lección: un XSS sigue siendo crítico aunque la cookie de sesión sea httpOnly —el navegador la adjunta en cada fetch same-origin— y el impacto real se cosecha pivotando a un recurso que la víctima privilegiada sí puede leer y el atacante no. Remediación: actualizar el editor a la versión con el fix, no preservar data-trix-* a ciegas (validar/allowlist-ar el contenido serializado), aplicar una CSP estricta sin handlers inline, y renderizar el contenido en la revisión con el mismo perfil endurecido que la vista pública en lugar de re-hidratar un editor vivo solo para mostrar.
Laboratorio basado en la reproducción anonimizada (empresa ficticia Redactia) de una técnica real de mutation XSS sobre el serializador del editor open-source Trix.