
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:
El punto importante es este:
el servidor aceptó un objetivo de sobrescritura elegido por el atacante, sin vincularlo a la página cuyo permiso de edición se había comprobado realmente.
Eso es una falla de control de acceso, no solo una mala higiene booleana.
El exploit era especialmente práctico para archivos adjuntos de diagramas.
El cliente de Docmost reutiliza intencionalmente attachmentId para guardar diagramas y usa nombres de archivo deterministas:
diagram.excalidraw.svgdiagram.drawio.svgEso importa porque reduce los requisitos del atacante.
Para archivos adjuntos genéricos, el atacante necesita ambos:
Para diagramas, el nombre de archivo ya es predecible.
Por lo tanto, si el atacante puede leer el contenido de la página de la víctima, a menudo puede recuperar la única pieza faltante que necesita:
attachmentId de la víctimaEn mi configuración de validación, usé exactamente esa ruta:
Eso fue suficiente.
El exploit cruzó los límites de página y los límites de espacio dentro del mismo espacio de trabajo, mientras seguía satisfaciendo la comprobación defectuosa del espacio de trabajo.
Validé el problema en vivo contra Docmost v0.70.3 usando un laboratorio desechable construido a partir de docmost/docmost:0.70.3, Postgres y Redis.
El flujo de la PoC fue:
attachmentId de la víctima.POST /api/files/upload con:
pageId = ID de la página del atacanteattachmentId = ID del archivo adjunto de la víctimafile = archivo de reemplazo controlado por el atacante usando el nombre de archivo de la víctimaLa forma mínima de la solicitud era:
POST /api/files/upload
Content-Type: multipart/form-data
pageId=<attackerPageId>
attachmentId=<victimAttachmentId>
[email protected];filename=diagram.excalidraw.svg
El resultado observado en vivo fue:
019d18ae-b176-751c-8525-b5f3cede131d019d18ae-b15b-70e9-ac67-64948e87cc5e019d18ae-b12f-75ec-8c1c-5aff3ba6be9c200 OK686a0a0ede90ece1cbb975bb29304a6c3a90373a9c3ab2496345cf7ca59cc8fa
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
Attacker replacement from another page
Eso es una prueba completa de sobrescritura de extremo a extremo, no solo una revisión teórica del código fuente.
Durante el triaje, usé dos estilos de prueba:
El harness independiente fue útil para aislar el fallo de lógica booleana.
La PoC HTTP en vivo fue el artefacto más sólido porque demostró la historia de seguridad completa:
attachmentId de la víctima fue aceptadoEsa distinción importa en errores de control de acceso.
"la condición está mal" no es suficiente por sí solo.
"la condición está mal y la aplicación puede ser llevada de extremo a extremo a una sobrescritura no autorizada persistente" es el caso completo.
La corrección se envió en v0.71.0 y cambió la guardia de sobrescritura de && a ||:
if (
existingAttachment.pageId !== pageId ||
existingAttachment.fileExt !== preparedFile.fileExtension ||
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
Ese parche es mínimo, directo y correcto para el error que se informó.
Restablece la regla correcta:
la sobrescritura solo se permite cuando el archivo adjunto existente coincide exactamente con las suposiciones de página/espacio de trabajo/tipo autorizadas.
Una vez que la guardia rechaza cualquier discrepancia:
Este fue el tipo correcto de corrección:
Solo un vínculo estricto entre la página autorizada y el objetivo de sobrescritura.
Todavía queda una lección de ingeniería más amplia:
los endpoints de carga genéricos que también sirven flujos de actualización in situ deben tratarse como superficies API de alto riesgo.
Incluso cuando el error inmediato se corrige, los diseños más sólidos a largo plazo son:
Pero para la vulnerabilidad en sí, el parche publicado cerró el problema principal limpiamente.
Independientemente de si el proyecto añadió sus propias pruebas privadas en torno a la corrección, estos son los casos que importan para la cobertura a largo plazo:
El objetivo de estas pruebas no es solo la corrección.
Es fijar la vinculación de autorización para que futuras refactorizaciones "útiles" de carga no reabran la misma clase de error.
El aviso de seguridad publicado clasificó este problema como:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
Eso da un puntaje de 7.1 / Alto, que es la conclusión correcta.
La métrica importante aquí es Integridad.
Esto no era un error de metadatos de bajo nivel. El atacante controlaba completamente los bytes de reemplazo escritos en la ruta del archivo adjunto de otra página, y la página de la víctima continuaba sirviendo el objeto modificado después.
Eso es exactamente el tipo de manipulación entre registros almacenados que merece una Integridad Alta.
Que la Disponibilidad se mantenga en Baja también tiene sentido porque corromper un diagrama o documento adjunto puede hacer que el contenido de la víctima sea inutilizable, pero el impacto principal sigue siendo la modificación no autorizada en lugar de una interrupción completa del servicio.
Informé el problema de forma privada a través de los Avisos de Seguridad de GitHub con:
El problema fue aceptado por el mantenedor, asignado CVE-2026-34213, y publicado el 14 de abril de 2026.
El aviso público enumera:
>= v0.3.0v0.71.0Ese historial también coincidió con mi revisión local del código fuente: la lógica de sobrescritura vulnerable estaba presente en la versión etiquetada más antigua que revisé en la línea vulnerable.
La lección interesante aquí no es meramente "usa || en lugar de &&."
Eso es el síntoma.
La lección más profunda es:
si un campo controlado por el usuario prueba la autorización y otro campo controlado por el usuario selecciona el objeto que se está actualizando, esos dos campos deben vincularse explícita y exactamente.
Esa regla aparece en todas partes:
En el momento en que un sistema dice:
ha creado un límite de seguridad que debe hacerse cumplir con invariantes de coincidencia exacta.
Cualquier cosa más suave que eso se convierte tarde o temprano en un error de clave controlada por el usuario.
Este problema también refuerza un segundo punto que es fácil de subestimar:
los pequeños errores booleanos en el código de protección pueden tener consecuencias de seguridad de primer orden.
Una condición de tres cláusulas que "parece razonable" a simple vista fue suficiente para invertir el modelo de protección de la ruta de sobrescritura.
Por eso estas superficies merecen una revisión deliberada en lugar de una confianza casual.
pageId proporcionado por el llamador, pero la selección del objetivo de sobrescritura usó un attachmentId separado proporcionado por el llamador.v0.71.0 cambió correctamente la guardia para rechazar ante cualquier discrepancia.Esta vulnerabilidad no trataba sobre un comportamiento de almacenamiento exótico.
Trataba sobre una ruta de actualización que confiaba en un identificador de objeto seleccionado por el atacante más de lo que debería.
Docmost probó el acceso de edición en una página, aceptó un ID de archivo adjunto existente de otra página, y luego dejó que una comprobación de sobrescritura defectuosa convirtiera esa discrepancia en un reemplazo exitoso de archivo entre páginas.
Por eso se convirtió en CVE-2026-34213.
El parche en v0.71.0 corrigió el problema inmediato limpiamente, pero la lección más amplia sigue siendo valiosa:
cuando la autorización y la selección de objetos se dividen en campos separados controlados por el usuario, la vinculación exacta es la propiedad de seguridad.