Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2026-34212 — Docmost aceptó una URL javascript: dentro de un nodo de adjunto, la preservó a través del almacenamiento y la representación, y la convirtió de nuevo en un ancla cliqueable en el origen de Docmost. | Kitploit
Herramientas/GitHubGitHub/0xmrma/cve-2026-34212
Análisis de VulnerabilidadesExplotaciónSeguridad WebPruebas de PenetraciónPapers e InvestigaciónAprendizaje y Educación
GitHub0xmrma/cve-2026-34212

CVE-2026-34212

Docmost aceptó una URL javascript: dentro de un nodo de adjunto, la preservó a través del almacenamiento y la representación, y la convirtió de nuevo en un ancla cliqueable en el origen de Docmost.

Ver Repositorio
1hace 2 mesesAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

CVE-2026-34212

Docmost aceptó una URL javascript: dentro de un nodo de adjunto, la preservó durante el almacenamiento y renderizado, y la devolvió a un ancla enlaceable en el origen de Docmost.

Introducción

Identifiqué, divulgué responsablemente y reproduje un problema de XSS almacenado de gravedad Alta en Docmost, la plataforma colaborativa de documentación de código abierto.

El sitio oficial de Docmost la presenta como una wiki local lista para la empresa con más de 3 millones de descargas, y dice que es confiada por equipos de organizaciones como Vilnius City, Bechtle, el Gobierno Australiano, la Cruz Roja y ETS Quebec.

El error se encontraba en un lugar fácil de pasar por alto en los sistemas de texto enriquecido:

no en la extensión de enlace normal, sino en un tipo de nodo personalizado separado utilizado para archivos adjuntos.

Estaba revisando la tubería del editor con una pregunta muy específica en mente:

si los enlaces normales bloquean las URLs javascript:, ¿los nodos de adjuntos aplican la misma regla antes de llegar a un sumidero de ancla?

En versiones vulnerables, no lo hacían.

Docmost aceptaba un nodo de adjunto malicioso en el JSON de la página, almacenaba su atributo url sin cambios, y luego renderizaba ese valor de vuelta a un elemento <a href="javascript:..."> enlaceable.

Ese problema se convirtió en CVE-2026-34212.

Docmost: docmost/docmost
Aviso: GHSA-cf68-cff9-hq4w
CVE: CVE-2026-34212
Corregido en: v0.71.0

foto0

Cadena de Ataque

URL del nodo de adjunto controlada por atacante -> JSON de página aceptado y almacenado sin cambios -> Renderizado HTML/React convierte esa URL en href de ancla -> víctima hace clic en la acción del adjunto -> JavaScript controlado por atacante se ejecuta en el origen de Docmost


Qué Hace Esta Parte de Docmost

Docmost almacena el contenido de la página en un formato JSON compatible con ProseMirror/Tiptap.

Ese modelo de contenido incluye nodos de bloque personalizados para cosas como:

  • imágenes
  • diagramas
  • incrustaciones
  • adjuntos

El nodo de adjunto almacena campos como:

  • url
  • name
  • mime
  • size
  • attachmentId

El servidor acepta contenido de página en varios formatos:

  • json
  • markdown
  • html

y lo normaliza a JSON de ProseMirror antes de almacenarlo.

Eso significa que cualquier tipo de nodo que pueda transportar una URL es parte de un límite de confianza directo.

Si uno de esos tipos de nodo eventualmente se renderiza en un <a href>, el manejo del esquema de URL no es opcional. Es parte del modelo de seguridad.


Por Qué Esta Superficie Valía la Pena Investigarla

Las extensiones personalizadas del editor son una fuente frecuente de desviación en la seguridad.

El sistema base puede saber cómo manejar URLs peligrosas correctamente, pero cada nodo personalizado aún debe reaplicar las mismas reglas en sus propios sumideros.

Eso crea una estrategia de revisión predecible:

  • encontrar cada tipo de nodo que almacene un campo similar a una URL
  • rastrear dónde se acepta ese campo
  • rastrear dónde se renderiza ese campo
  • comparar su comportamiento de sanitización con el manejo normal de enlaces de la plataforma

Eso es exactamente lo que expuso este error.

La extensión de enlace normal de Docmost ya trataba javascript: como peligroso.

Su nodo de adjunto no lo hacía.

Una vez que ves esa asimetría, la pregunta de seguridad se vuelve obvia:

¿puedo persistir un nodo de adjunto cuya url es javascript: y conseguir que se renderice de vuelta en un ancla en vivo?

La respuesta fue sí.


Causa Raíz

La causa raíz fue la sanitización inconsistente de URLs entre tipos de nodos de contenido.

La ruta de contenido del lado del servidor aceptaba URLs de adjunto arbitrarias siempre que el contenido general coincidiera con el esquema de ProseMirror.

En la versión vulnerable:

  • CreatePageDto aceptaba content?: string | object
  • PageService.parseProsemirrorContent() normalizaba markdown, html o json
  • el servidor luego llamaba a jsonToNode(prosemirrorJson)
  • si la validación del esquema pasaba, el contenido se almacenaba

Ese paso de validación verificaba la validez estructural, no la seguridad de la URL.

La parte crítica de la lógica vulnerable del servidor era efectivamente:

root@kitploit:~
prosemirrorJson = content;
jsonToNode(prosemirrorJson);
return prosemirrorJson;

No ocurría ninguna normalización del esquema de URL de adjunto allí.

Más tarde, la extensión de adjunto renderizaba el valor controlado por el atacante directamente.

El nodo de adjunto vulnerable hacía esto:

root@kitploit:~
url: {
  default: "",
  parseHTML: (element) => element.getAttribute("data-attachment-url"),
  renderHTML: (attributes) => ({
    "data-attachment-url": attributes.url,
  }),
},

y luego:

root@kitploit:~
[
  "a",
  {
    href: HTMLAttributes["data-attachment-url"],
    class: "attachment",
    target: "blank",
  },
  `${HTMLAttributes["data-attachment-name"]}`,
]

En el lado del cliente, la vista de nodo React lo envolvía de nuevo en:

root@kitploit:~
<a href={getFileUrl(url)} target="_blank">

Pero getFileUrl() solo tenía casos especiales para:

  • URLs absolutas http
  • /api/...
  • /files/...

Cualquier otra cosa se devolvía sin cambios.

Así que una carga útil como:

root@kitploit:~
javascript:alert(document.domain)

sobrevivía a:

  • almacenamiento JSON
  • validación del esquema del lado del servidor
  • renderizado HTML
  • manejo de URL del lado del cliente

Eso solo ya sería suficiente para XSS almacenado.

Lo que hace especialmente clara la causa raíz es el punto de comparación.

La extensión de enlace normal de Docmost bloqueaba explícitamente javascript::

  • rechazaba javascript: en parseHTML()
  • vaciaba un href javascript: en renderHTML()

Así que el producto ya sabía que este esquema era peligroso.

El nodo de adjunto simplemente no aplicaba la misma política.

Por eso esto no era "XSS genérico en el editor".

Era una brecha en el límite de confianza específica del nodo.


Por Qué Esto Es un Problema de Seguridad, No Solo una Sanitización Faltante

Este error no se trataba meramente de una estética HTML insegura.

Permitía que un atacante que pudiera editar una página persistiera una carga útil maliciosa que luego se ejecutaría en el origen de Docmost cuando otro usuario interactuara con el adjunto renderizado.

Eso importa porque un script en el origen puede:

  • leer datos a los que la víctima tiene acceso
  • emitir solicitudes autenticadas como la víctima
  • modificar contenido que la víctima tiene permitido modificar
  • abusar de cualquier superficie DOM o API expuesta a la sesión

El requisito de un clic no reduce esto a un problema trivial.

El clic es parte del comportamiento normal del producto: la UI presenta intencionalmente el adjunto como un enlace/icono accionable.

Así que la pregunta de seguridad no es "¿puede el atacante forzar JS arbitrario sin ninguna interacción?"

La verdadera pregunta es:

¿almacena la aplicación contenido controlado por atacante que contiene scripts y luego lo presenta de vuelta a otros usuarios como una ruta de interacción confiable?

En versiones vulnerables, lo hacía.

Eso es XSS almacenado.


Por Qué la Explotación Era Práctica

La ruta de explotación era directa:

  • cualquier usuario con derechos de edición de página podía plantar la carga útil
  • la URL maliciosa sobrevivía al almacenamiento sin cambios
  • la página se renderizaba normalmente
  • los espectadores solo necesitaban acceso estándar a la página
  • un clic en la acción del adjunto era suficiente para desencadenar la ejecución

Esto también convertía a los usuarios con privilegios más altos en objetivos realistas.

Si el propietario del espacio de trabajo, un administrador o un editor ampliamente confiado veía contenido controlado por atacante y hacía clic en la acción del adjunto, el script del atacante se ejecutaría en ese contexto de sesión más privilegiado.

Ese es el punto práctico importante:

el requisito de privilegio del atacante era solo bajo. El nivel de privilegio de la víctima determinaba cuánto valor llevaba la sesión XSS.


Prueba de Concepto

Validé el problema en vivo contra Docmost v0.70.3.

La PoC solo usaba solicitudes HTTP normales y las API de página de la aplicación.

El flujo era:

  1. Iniciar sesión como un usuario que puede editar una página.
  2. Crear o seleccionar una página.
  3. Enviar POST /api/pages/update con format: "json" y un nodo de adjunto cuya url es una carga útil javascript:.
  4. Solicitar la página de vuelta a través de POST /api/pages/info.
  5. Confirmar que el JSON almacenado aún contiene la URL maliciosa.
  6. Solicitar la misma página en formato HTML y confirmar que el servidor devuelve un ancla cuyo href sigue siendo javascript:....
  7. En la UI, un espectador que haga clic en la acción del adjunto renderizado ejecuta la carga útil en el origen de Docmost.

El contenido malicioso mínimo era:

root@kitploit:~
{
  "pageId": "<pageId>",
  "content": {
    "type": "doc",
    "content": [
      {
        "type": "attachment",
        "attrs": {
          "url": "javascript:alert(document.domain)",
          "name": "policy.pdf",
          "mime": "application/pdf",
          "size": 1
        }
      }
    ]
  },
  "operation": "replace",
  "format": "json"
}

El resultado en vivo observado de mi prueba fue:

  • la API aceptó el nodo de adjunto malicioso sin cambios
  • el ID de página almacenado fue 019d18cf-4212-70b0-894a-fe20080fb0f1
  • POST /api/pages/info devolvió el JSON almacenado con:
root@kitploit:~
"url": "javascript:alert(document.domain)"
  • POST /api/pages/info con format: "html" devolvió HTML que contenía:
root@kitploit:~
<div data-type="attachment" data-attachment-url="javascript:alert(document.domain)" data-attachment-name="policy.pdf" data-attachment-mime="application/pdf" data-attachment-size="1"><a href="javascript:alert(document.domain)" class="attachment" target="blank">policy.pdf</a></div>

Esa respuesta HTML es la prueba crítica.

No necesitaba basarme en una afirmación vaga de que "un navegador podría hacer algo interesante."

La propia aplicación renderizaba el sumidero ejecutable exacto.

Una vez que un usuario hace clic en ese enlace/icono de adjunto, el navegador ejecuta la URL javascript: en el origen de la página que la creó.


Por Qué se Eligió la PoC de Esta Manera

Para XSS impulsado por editor, las capturas de pantalla por sí solas son evidencia débil.

Muestran síntomas, no la falla del límite.

Por eso estructure la PoC alrededor de dos puntos de verificación explícitos:

  1. prueba de almacenamiento
  2. prueba de sumidero renderizado

La prueba de almacenamiento mostró que el servidor aceptó y preservó el esquema peligroso.

La prueba de sumidero renderizado mostró que la aplicación convirtió ese valor almacenado de vuelta en:

root@kitploit:~
<a href="javascript:...">

Esa división importa.

Si un producto almacena entrada peligrosa pero la neutraliza antes de cada sumidero, puedes tener una brecha de endurecimiento pero no necesariamente un XSS vivo.

Si el producto almacena entrada peligrosa y luego la renderiza en un sumidero de ejecución real, tienes la cadena completa de vulnerabilidad.

Eso es lo que pasó aquí.


Análisis de la Corrección

La corrección se envió en v0.71.0 y abordó la ruta de explotación renderizada aplicando sanitización de URL a las URLs de adjuntos.

La extensión de adjunto ahora importa y usa sanitizeUrl, incluyendo:

  • sanitizar data-attachment-url durante el análisis
  • sanitizar data-attachment-url durante el renderizado
  • sanitizar el href del ancla

Conceptualmente, el parche cambió el nodo de adjunto de:

  • confiar en la URL de adjunto sin procesar
  • emitir la URL de adjunto sin procesar

a:

  • normalizar la URL de adjunto antes de que se convierta en parte del nodo renderizado

El ayudante del lado del cliente getFileUrl() también se actualizó para que los esquemas desconocidos ya no pasen sin cambios. En la versión parcheada, la ruta de respaldo devuelve sanitizeUrl(src) en lugar de devolver src tal cual.

Esa es una parte importante de la corrección porque el diseño vulnerable tenía dos problemas que se reforzaban mutuamente:

  • el nodo renderizaba un href sin procesar
  • el respaldo del cliente trataba los esquemas desconocidos como aceptables

El parche eliminó ambas suposiciones.

Esta fue una buena corrección para la ruta de XSS en vivo porque alineó el manejo de URL de adjuntos con el resto del modelo de seguridad del editor.

Dicho esto, todavía hay una lección de endurecimiento más amplia:

la sanitización del lado del cliente o en tiempo de renderizado es necesaria aquí, pero el rechazo del lado del servidor de esquemas peligrosos durante la creación/actualización de páginas sería un invariante aún más fuerte.

El modelo más seguro a largo plazo es:

  • rechazar esquemas obviamente peligrosos en la ingesta
  • sanitizar nuevamente en los límites de renderizado

La defensa en profundidad importa en los sistemas de contenido enriquecido.


Casos de Regresión Que Importan

Para una cobertura a largo plazo, estos son los casos que más importan:

  • actualizaciones de página JSON que contienen attachment.attrs.url = "javascript:..."
  • importaciones HTML que contienen data-attachment-url="javascript:..."
  • el renderizado de adjuntos nunca debe emitir href="javascript:..."
  • los ayudantes de respaldo del cliente no deben devolver esquemas ejecutables desconocidos sin cambios
  • los nodos de adjuntos y los nodos de enlace normales deben compartir una política de esquema de URL equivalente
  • las rutas de adjuntos internos seguros como /api/files/... y /files/... deben seguir funcionando normalmente

El punto clave es la consistencia.

Si los enlaces normales se sanitizan pero los nodos personalizados que llevan URL no, el editor realmente no tiene una política de seguridad de URL única.

Tiene fragmentos, y los fragmentos son donde viven los errores XSS.


Severidad y Clasificación

El aviso publicado clasificó este problema como:

  • CWE-79: Neutralización incorrecta de la entrada durante la generación de páginas web
  • CVSS v3.1:
root@kitploit:~
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N

Eso se sitúa en 7.6 / Alto.

Esta es una clasificación defendible.

Las propiedades importantes son:

  • requisito de privilegio bajo del atacante
  • carga útil almacenada
  • ejecución en el origen de Docmost
  • cambio de alcance
  • impacto de confidencialidad significativo porque el script puede acceder a datos visibles para la víctima dentro de la aplicación

La interacción del usuario sigue siendo necesaria porque la víctima tiene que activar el enlace/icono del adjunto. Por eso UI:R es correcto.

Pero una vez que ocurre esa interacción, el límite de seguridad ya ha fallado mucho antes: la aplicación almacenó un esquema peligroso y lo renderizó de vuelta en un sumidero de ejecución.


Divulgación

Reporté el problema de forma privada a través de GitHub Security Advisories con:

  • análisis de causa raíz
  • una PoC HTTP en vivo
  • evidencia JSON almacenada
  • evidencia de sumidero HTML renderizado
  • un laboratorio de prueba desechable fijado

El problema fue aceptado, asignado CVE-2026-34212, y publicado el 14 de abril de 2026.

El aviso público actualmente lista:

  • versión afectada: 0.70.3
  • versión corregida: 0.71.0

Mi validación en vivo se realizó en v0.70.3, que coincidía con la versión vulnerable publicada.


Qué Enseña Realmente Este Error

La lección principal no es simplemente "sanitizar URLs".

Todo el mundo ya lo sabe.

La lección más interesante es:

si una aplicación tiene un tipo de nodo que lleva URL seguro y otro inseguro, el inseguro es la política real.

Los sistemas de texto enriquecido a menudo acumulan extensiones personalizadas más rápido de lo que acumulan revisión de seguridad.

Eso crea exactamente este tipo de asimetría:

  • la ruta de enlace estándar está endurecida
  • la ruta de adjunto se trata como "interna" o "especial"
  • la ruta especial se convierte silenciosamente en el sumidero XSS más fácil

Este error también muestra por qué la validación de esquema no es suficiente.

jsonToNode() verificó que el contenido era estructuralmente válido como datos de ProseMirror. No demostró que el contenido fuera seguro de renderizar.

Esas son preguntas diferentes.

La revisión de seguridad se vuelve mucho más precisa cuando mantienes esas preguntas separadas:

  • ¿es este contenido estructuralmente válido?
  • ¿es este contenido seguro de almacenar?
  • ¿es este contenido seguro de renderizar en cada sumidero?

El nodo de adjunto pasó la primera pregunta y falló la tercera.

Así es como los errores de contenido almacenado sobreviven dentro de tuberías de editor por lo demás bien estructuradas.


Puntos Clave

  • Docmost aceptaba URLs de nodos de adjunto sin procesar en el contenido de la página.
  • La validación de página del lado del servidor verificaba la forma del esquema de ProseMirror, no la seguridad del esquema de URL.
  • El nodo de adjunto vulnerable renderizaba data-attachment-url y href del ancla directamente desde la entrada controlada por el atacante.
  • El ayudante del cliente getFileUrl() devolvía esquemas desconocidos sin cambios.
  • Los nodos de enlace normales ya bloqueaban javascript:, pero los nodos de adjunto no.
  • Un editor con pocos privilegios podía plantar la carga útil una vez y apuntar a espectadores posteriores.
  • La PoC en vivo demostró tanto la persistencia almacenada como el sumidero ejecutable renderizado.
  • La corrección en v0.71.0 agregó manejo de sanitizeUrl al nodo de adjunto y a la ruta de respaldo del cliente.

Palabras Finales

Esta vulnerabilidad no se trataba de una peculiaridad del navegador.

Se trataba de un nodo de contenido personalizado que eludía las propias suposiciones de seguridad de URL de la aplicación.

Docmost aceptó una URL de adjunto controlada por el atacante, la preservó durante el almacenamiento y luego la renderizó de vuelta en un ancla en vivo en el origen de la aplicación.

Por eso se convirtió en CVE-2026-34212.

El parche en v0.71.0 cerró limpiamente la ruta de XSS activa, pero la lección más amplia es la que vale la pena conservar:

en aplicaciones con mucho editor, cada nodo personalizado que puede llevar una URL es su propio límite de seguridad, y debe revisarse como tal.

Descargar herramienta