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.
Learn to find this bug
A real HackerOne report, reproduced for you to practice.
Create your account and practice real bugs that got paid. Download the environment, find it and learn the exact technique — your path to your first bounty.
hunters training
labs from real reports
completions
in bounties practiced
Access to all labs · no commitment · cancel anytime
Hunters who solved it
Be the first to solve it
Your name could show up here, at the very top.
Objectives
Achievement you'll earn
Solve this lab to unlock this shareable achievement
Mutation XSS en el editor Trix (bypass sanitizer↔serializer)
Community writeups
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.