
Un recurso compartido público parecía limpio en el árbol de páginas, pero el endpoint de búsqueda contó una historia diferente. En Docmost, las páginas secundarias restringidas ocultas a los espectadores del recurso compartido público aún podían filtrarse a través de los resultados de búsqueda del recurso compartido público.
Un recurso compartido público se veía limpio en el árbol de páginas, pero el endpoint de búsqueda contaba una historia diferente. En Docmost, las páginas hijas restringidas ocultas a los visores de recursos compartidos públicos aún podían filtrarse a través de los resultados de búsqueda del recurso compartido público.
Encontré este problema mientras revisaba Docmost, una plataforma colaborativa de wiki y documentación de código abierto, con una pregunta muy simple en mente:
Si una página se oculta intencionalmente a un visor de un recurso compartido público, ¿cada funcionalidad pública respeta ese mismo límite de restricción?
En este caso, la respuesta fue no.
Una página hija restringida podía permanecer oculta en el árbol del recurso compartido público mientras se filtraba a través del endpoint de búsqueda del recurso compartido público.
El problema fue aceptado y se le asignó CVE-2026-33146.
Docmost: Docmost en GitHub
CVE: CVE-2026-33146
El sitio oficial de Docmost lo presenta como una wiki local preparada para empresas con más de 3 millones de descargas, y afirma que es utilizado por equipos de organizaciones como Vilnius City, Bechtle, el Gobierno Australiano, la Cruz Roja y ETS Quebec.
recurso compartido público principal con subpáginas habilitadas → descendiente restringido omitido del árbol público → atacante consulta la búsqueda del recurso compartido público → se filtran el título y el fragmento de la página hija restringida
Docmost es una plataforma colaborativa de wiki y documentación.
Proporciona:
Eso significa que su modelo de recurso compartido público es un límite de seguridad real.
La pregunta importante aquí no era si Docmost puede compartir páginas públicamente.
La pregunta real era:
Cuando Docmost decide que una página descendiente está restringida y no debería aparecer a un visitante del recurso compartido público, ¿esa restricción se mantiene en todas partes del flujo del recurso compartido público?
En este caso, no fue así.
Muchas revisiones de seguridad se detienen demasiado pronto cuando ven una página oculta en la interfaz de usuario.
Eso no es suficiente.
La pregunta más sólida es esta:
¿Cada ruta del backend aplica la misma decisión de visibilidad?
Esto importa porque los límites de seguridad no se definen por cómo se ve la interfaz.
Se definen por lo que el servidor realmente devuelve.
Aquí, el endpoint del árbol público se comportó de manera segura:
Pero la ruta de búsqueda del recurso compartido público se comportó de manera diferente:
Eso convirtió esto en un problema real de autorización y divulgación de información, no solo en una inconsistencia de presentación.
No abordé Docmost fuzzeando rutas al azar y esperando que apareciera algo interesante.
El camino más sólido fue elegir primero un límite de confianza.
Para aplicaciones que admiten:
una de las mejores preguntas que hacer es:
¿La capa de búsqueda aplica exactamente el mismo límite de autorización que la capa de navegación?
Esa pregunta se vuelve especialmente valiosa cuando:
Aquí es exactamente donde apareció este problema.
El error no fue que Docmost no ocultara la página restringida en el árbol público normal.
El error fue que la búsqueda pública no respetaba esa misma lógica de restricción.
Según la revisión del código fuente, el flujo del árbol público utilizaba un recorrido de descendientes consciente de restricciones.
Área relevante:
apps/server/src/core/share/share.service.tsEsa ruta excluía intencionalmente a los descendientes restringidos usando:
getPageAndDescendantsExcludingRestricted(...)Pero el flujo de búsqueda del recurso compartido público seguía una ruta diferente.
Áreas relevantes:
apps/server/src/core/search/search.controller.tsapps/server/src/core/search/search.service.tsAllí, el código recopilaba descendientes usando:
getPageAndDescendants(...)Eso significaba que los descendientes restringidos permanecían en el ámbito de la búsqueda.
En el contexto del recurso compartido público, eso importa mucho porque la rama de búsqueda se ejecuta sin un contexto de permisos de usuario autenticado normal. Por lo tanto, una vez que los descendientes restringidos se incluían en el conjunto de páginas buscables, sus metadatos podían filtrarse a través de la respuesta.
Porque el atacante no necesita una cuenta autenticada.
Solo necesita:
Una vez que existe esa condición, un visitante público puede consultar el endpoint de búsqueda del recurso compartido y recuperar:
Eso es suficiente para crear una fuga de confidencialidad, incluso si no se devuelve el cuerpo completo de la página.
La distinción importante es que la aplicación ya señala claramente el modelo de seguridad previsto.
El endpoint del árbol público oculta a los descendientes restringidos.
Por lo tanto, la pregunta real no es:
"¿La búsqueda devuelve un conjunto de resultados más amplio?"
La pregunta real es:
"¿La búsqueda está violando una decisión de autorización ya aplicada en otro lugar para el mismo límite de recurso compartido público?"
En Docmost, lo estaba.
Eso convierte esto de:
en:
Por eso es una vulnerabilidad real.
Validé el problema comparando los dos endpoints públicos relevantes uno al lado del otro.
Primero, probé el endpoint normal del árbol público usando la clave del recurso compartido público.
Ejemplo de solicitud:
POST /api/shares/tree HTTP/1.1
Host: 127.0.0.1:6752
Content-Type: application/json
{
"shareId": "public-share-key"
}
La respuesta devolvió solo la página hija pública en el árbol de páginas.
Resultado representativo:
{
"pageTree": [
{
"id": "public-child",
"title": "Public roadmap"
}
]
}
Eso estableció el comportamiento esperado del producto:
Luego consulté el endpoint de búsqueda del recurso compartido público usando un término que aparecía dentro del descendiente restringido.
Ejemplo de solicitud:
POST /api/search/share-search HTTP/1.1
Host: 127.0.0.1:6752
Content-Type: application/json