
Пользователь Docmost с низкими привилегиями может указать attachmentId жертвы в общей конечной точке загрузки и перезаписать сохраненное вложение другой страницы в пределах того же рабочего пространства.
Низкопривилегированный пользователь Docmost мог указать чужой attachmentId в общем эндпоинте загрузки и перезаписать сохранённое вложение другой страницы в том же рабочем пространстве.
Я выявил, ответственно раскрыл и воспроизвёл высокосерьёзную ошибку авторизации в Docmost — платформе для совместной работы с документацией с открытым исходным кодом.
Официальный сайт Docmost представляет её как корпоративную вики для локального развёртывания с 3M+ загрузок и утверждает, что ей доверяют команды таких организаций, как Vilnius City, Bechtle, Правительство Австралии, Красный Крест и ETS Quebec.
Ошибка находилась в общем пути загрузки файлов, который Docmost также использует для сохранения/обновления диаграмм.
Я изучал этот код, задавшись очень конкретным вопросом:
Что произойдёт, если эндпоинт загрузки подтвердит доступ на редактирование одной страницы, а цель перезаписи будет выбрана отдельным контролируемым пользователем идентификатором вложения?
В данном случае этот вопрос напрямую привёл к реальной ошибке связывания объектов.
Docmost позволял вызывающему коду отправлять:
pageId для страницы, которую ему разрешено редактировать, иattachmentId, принадлежащий другой странице в том же рабочем пространствеСервер выполнял проверку согласованности перезаписи, но защита использовала неверную логическую операцию.
Это означало, что запрос мог пройти авторизацию и всё равно перезаписать чужое вложение.
Эта проблема получила номер CVE-2026-34213.
Docmost: docmost/docmost
Уведомление: GHSA-89fp-2hch-j9gp
CVE: CVE-2026-34213
Исправлено в: v0.71.0
---
контролируемый атакующим pageId с доступом на редактирование -> контролируемый атакующим чужой attachmentId -> ошибочная защита перезаписи считает межстраничную перезапись допустимой -> путь к хранилищу перестраивается на основе чужого attachmentId -> байты атакующего заменяют файл жертвы -> страница жертвы продолжает отдавать изменённое вложение
Docmost хранит загруженные вложения страниц как записи в базе данных, а также соответствующие файлы в хранилище.
При обычной загрузке сервер создаёт новый идентификатор вложения и записывает новый файл.
Однако для сохранения/обновления диаграмм клиент намеренно использует существующий attachmentId, чтобы один и тот же файл диаграммы можно было обновить на месте, а не создавать каждый раз новую запись вложения.
Такое поведение само по себе легитимно.
Проблема в том, что оно создаёт путь с высоким риском:
Всякий раз, когда эндпоинт смешивает эти две обязанности, реализация должна точно связывать их вместе.
Docmost этого не сделал.
Смешанные эндпоинты создания/обновления — частое место ошибок авторизации.
Причина проста:
Именно такой шаблон здесь и есть.
POST /api/files/upload проверял, может ли вызывающий редактировать страницу, указанную в pageId.
Но если также указывался attachmentId, сервер переключался на путь перезаписи и выбирал существующую запись вложения отдельно.
Это порождало критический вопрос безопасности:
доказывает ли путь перезаписи, что выбранное вложение действительно принадлежит авторизованной странице?
Ответ в уязвимых версиях был: нет.
Коренная причина — обход авторизации через контролируемый пользователем ключ в сочетании с ошибкой логической операции в защите перезаписи.
Уязвимый поток выглядел так:
AttachmentController.uploadFile() считывал pageId из данных multipart-формы.validateCanEdit(page, user).attachmentId из того же запроса.AttachmentService.uploadFile() загружал существующее вложение по этому указанному атакующим ID.&& вместо того, чтобы отклонять при любом несовпадении.Уязвимая защита была:
if (
existingAttachment.pageId !== pageId &&
existingAttachment.fileExt !== preparedFile.fileExtension &&
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
Это условие отклоняло запрос только если:
одновременно.
Это противоположно тому, что должна делать защита перезаписи.
Для реального случая атаки атакующий намеренно оставался в пределах одного рабочего пространства.
Итак:
existingAttachment.workspaceId !== workspaceId было falseКак только этот операнд становился ложным, всё условие && оценивалось как false, даже если вложение принадлежало другой странице.
Таким образом, сервер считал межстраничную перезапись допустимой.
Это была первая половина ошибки.
Вторая половина — то, что делало воздействие реальным.
После проверки сервис перестраивал путь к целевому хранилищу, используя указанный атакующим attachmentId и имя файла:
const filePath =
`${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/` +
`${attachmentId}/${preparedFile.fileName}`;
Затем на пути обновления Docmost обновлял только изменяемые метаданные, такие как:
fileSizeupdatedAtОн не привязывал повторно владельца к странице атакующего.
Таким образом, страница жертвы продолжала указывать на ту же запись вложения и тот же идентификатор вложения. Изменились только байты файла.
Вот почему это была не безобидная нестыковка.
Это была примитивная постоянная несанкционированная перезапись.
Это не была косметическая ошибка и не проблема коллизии имён файлов.
Атакующему не нужна гонка. Атакующему не нужно угадывать случайный путь. Атакующему не нужен доступ на запись к странице жертвы.
Ему нужно было только:
Отсюда он мог заменить байты сохранённого файла для вложения другой страницы, в то время как страница жертвы продолжала ссылаться на это вложение и отдавать его так, как будто ничего не изменилось.
Это прямой сбой целостности.
На практике атакующий мог:
Важный момент:
сервер принял цель перезаписи, выбранную атакующим, без привязки к странице, чьё разрешение на редактирование было фактически проверено.
Это сбой контроля доступа, а не просто плохая логическая гигиена.
Эксплойт был особенно практичен для вложений-диаграмм.
Клиент Docmost намеренно повторно использует attachmentId для сохранения диаграмм и использует детерминированные имена файлов:
diagram.excalidraw.svgdiagram.drawio.svgЭто важно, потому что снижает требования к атакующему.
Для обычных вложений атакующему нужны оба: