
Un usuario de Docmost con pocos privilegios podría proporcionar un attachmentId de una víctima al endpoint de carga genérico y sobrescribir el adjunto almacenado de otra página dentro del mismo espacio de trabajo.
Un usuario de Docmost con pocos privilegios podría proporcionar un attachmentId de una víctima al endpoint de carga genérico y sobrescribir el archivo adjunto de otra página dentro del mismo espacio de trabajo.
Identifiqué, divulgué responsablemente y reproduje una falla de autorización de severidad Alta en Docmost, la plataforma de documentación colaborativa de código abierto.
El sitio oficial de Docmost la presenta como una wiki local preparada para empresas con más de 3 millones de descargas, y afirma que es utilizada por equipos en organizaciones como Vilnius City, Bechtle, el Gobierno Australiano, la Cruz Roja y ETS Quebec.
El error residía en la ruta genérica de carga de archivos que Docmost también utiliza para los flujos de guardado/actualización de diagramas.
Estaba revisando ese código con una pregunta muy específica en mente:
¿Qué sucede si el endpoint de carga verifica el acceso de edición en una página, pero el objetivo de sobrescritura se selecciona con un ID de adjunto separado controlado por el usuario?
En este caso, esa pregunta condujo directamente a una falla real de vinculación de objetos.
Docmost permitía que un llamador enviara:
pageId de una página que tenía permiso para editar, yattachmentId perteneciente a una página diferente en el mismo espacio de trabajoEl servidor realizaba una comprobación de consistencia de sobrescritura, pero la guardia utilizaba la lógica booleana incorrecta.
Eso significaba que la solicitud podía pasar la autorización y sobrescribir el archivo adjunto de la víctima de todas formas.
Este problema se convirtió en CVE-2026-34213.
Docmost: docmost/docmost
Aviso de seguridad: GHSA-89fp-2hch-j9gp
CVE: CVE-2026-34213
Corregido en: v0.71.0
---
pageId controlado por el atacante con acceso de edición -> attachmentId de la víctima controlado por el atacante -> guardia de sobrescritura defectuosa trata la sobrescritura entre páginas como válida -> ruta de almacenamiento reconstruida a partir del attachmentId de la víctima -> bytes del atacante reemplazan el archivo de la víctima -> la página de la víctima continúa sirviendo el archivo adjunto modificado
Docmost almacena los archivos adjuntos subidos a las páginas como registros en la base de datos más archivos de respaldo en almacenamiento.
Para cargas normales, el servidor crea un nuevo ID de archivo adjunto y escribe un nuevo archivo.
Para los flujos de guardado/actualización de diagramas, sin embargo, el cliente reutiliza intencionalmente un attachmentId existente para que el mismo archivo de diagrama pueda actualizarse en el mismo lugar en lugar de generar un nuevo registro de archivo adjunto cada vez.
Ese comportamiento es legítimo por sí solo.
El problema es que crea una ruta de alto riesgo:
Siempre que un endpoint combina esas dos responsabilidades, la implementación debe vincularlas exactamente.
Docmost no lo hizo.
Los endpoints mixtos de creación/actualización son lugares comunes para errores de autorización.
La razón es simple:
Ese es exactamente el patrón aquí.
POST /api/files/upload validaba que el llamador pudiera editar la página nombrada por pageId.
Pero si también se proporcionaba attachmentId, el servidor cambiaba a una ruta de sobrescritura y seleccionaba un registro de archivo adjunto existente por separado.
Eso generó la pregunta crítica de seguridad:
¿la ruta de sobrescritura prueba que el archivo adjunto seleccionado realmente pertenece a la página autorizada?
La respuesta en las versiones vulnerables fue no.
La causa raíz fue una omisión de autorización a través de una clave controlada por el usuario, combinada con un error de lógica booleana en la guardia de sobrescritura.
El flujo vulnerable se veía así:
AttachmentController.uploadFile() leía pageId de los datos del formulario multipart.validateCanEdit(page, user).attachmentId opcional de la misma solicitud.AttachmentService.uploadFile() cargaba el archivo adjunto existente por ese ID proporcionado por el atacante.&& en lugar de rechazar ante cualquier discrepancia.La guardia vulnerable era:
if (
existingAttachment.pageId !== pageId &&
existingAttachment.fileExt !== preparedFile.fileExtension &&
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
Esa condición solo rechazaba la solicitud si:
todo al mismo tiempo.
Eso es lo opuesto a lo que debería hacer una guardia de sobrescritura.
Para el caso de ataque real, el atacante permanecía intencionalmente dentro del mismo espacio de trabajo.
Entonces:
existingAttachment.workspaceId !== workspaceId era falseUna vez que ese operando se volvía falso, toda la condición && se evaluaba como falsa, incluso si el archivo adjunto pertenecía a una página diferente.
Por lo tanto, el servidor trataba una sobrescritura entre páginas como válida.
Esa fue la primera mitad del error.
La segunda mitad es lo que hizo real el impacto.
Después de la comprobación, el servicio reconstruía la ruta de almacenamiento de destino usando el attachmentId y el nombre de archivo proporcionados por el atacante:
const filePath =
`${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/` +
`${attachmentId}/${preparedFile.fileName}`;
Luego, en la ruta de actualización, Docmost solo actualizaba metadatos mutables como:
fileSizeupdatedAtNo volvía a vincular la propiedad a la página del atacante.
Así que la página de la víctima seguía apuntando al mismo registro de archivo adjunto y al mismo ID de archivo adjunto. Solo cambiaban los bytes del archivo subyacente.
Por eso esto no era una discrepancia inofensiva.
Era una primitiva de sobrescritura no autorizada persistente.
Esto no era un error cosmético ni un problema de colisión de nombres de archivo.
El atacante no necesitaba una condición de carrera. El atacante no necesitaba adivinar una ruta aleatoria. El atacante no necesitaba acceso de escritura a la página de la víctima.
Solo necesitaba:
A partir de ahí, podía reemplazar los bytes del archivo almacenado para el archivo adjunto de otra página mientras la página de la víctima continuaba referenciando y sirviendo ese archivo adjunto como si nada hubiera cambiado.
Eso es una falla directa de integridad.
En términos prácticos, el atacante podía: