
Пользователь 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Это важно, потому что снижает требования к атакующему.
Для обычных вложений атакующему нужны оба:
Для диаграмм имя файла уже предсказуемо.
Поэтому, если атакующий может прочитать содержимое страницы жертвы, он часто может восстановить единственный недостающий элемент:
attachmentId жертвыВ моей тестовой среде я использовал именно этот путь:
Этого было достаточно.
Эксплойт пересекал границы страниц и границы пространств внутри одного рабочего пространства, всё ещё удовлетворяя ошибочной проверке рабочего пространства.
Я проверил проблему вживую на Docmost v0.70.3, используя одноразовую лабораторную среду, собранную из docmost/docmost:0.70.3, Postgres и Redis.
Поток PoC:
attachmentId жертвы.POST /api/files/upload с:
pageId = ID страницы атакующегоattachmentId = ID вложения жертвыfile = заменяющий файл атакующего с именем файла жертвыМинимальная форма запроса:
POST /api/files/upload
Content-Type: multipart/form-data
pageId=<attackerPageId>
attachmentId=<victimAttachmentId>
[email protected];filename=diagram.excalidraw.svg
Наблюдаемый живой результат:
019d18ae-b176-751c-8525-b5f3cede131d019d18ae-b15b-70e9-ac67-64948e87cc5e019d18ae-b12f-75ec-8c1c-5aff3ba6be9c200 OK686a0a0ede90ece1cbb975bb29304a6c3a90373a9c3ab2496345cf7ca59cc8fa
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
Attacker replacement from another page
Это полное сквозное доказательство перезаписи, а не просто теоретический обзор кода.
В ходе триажа я использовал два стиля доказательств:
Автономная обвязка была полезна для изоляции ошибки логической операции.
Живой HTTP PoC был более сильным артефактом, потому что он доказывал полную историю безопасности:
attachmentId жертвы был принятЭто различие имеет значение в ошибках контроля доступа.
«Условие неверно» само по себе недостаточно.
«Условие неверно, и приложение можно провести от начала до конца к постоянной несанкционированной перезаписи» — это полный случай.
Исправление было выпущено в v0.71.0 и изменило условие защиты перезаписи с && на ||:
if (
existingAttachment.pageId !== pageId ||
existingAttachment.fileExt !== preparedFile.fileExtension ||
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
Этот патч минимален, прямолинеен и корректен для сообщённой ошибки.
Он восстанавливает правильное правило:
перезапись разрешена только тогда, когда существующее вложение точно соответствует авторизованной странице/рабочему пространству/типу.
Как только защита отклоняет запрос при любом несовпадении:
Это был правильный вид исправления:
Просто строгая привязка между авторизованной страницей и целью перезаписи.
Тем не менее, здесь есть более широкий инженерный урок:
общие эндпоинты загрузки, которые также обслуживают потоки обновления на месте, следует рассматривать как поверхности API с высоким риском.
Даже когда непосредственная ошибка исправлена, более надёжными долгосрочными решениями являются:
Но для самой уязвимости опубликованный патч чисто закрыл основную проблему.
Независимо от того, добавил ли проект свои собственные закрытые тесты для исправления, вот сценарии, которые важны для долгосрочного покрытия:
Смысл этих тестов — не просто корректность.
Они нужны, чтобы закрепить привязку авторизации, чтобы будущие «полезные» рефакторинги загрузки не открыли тот же класс ошибок заново.
Опубликованное уведомление классифицировало эту проблему как:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
Это даёт 7.1 / High, что является правильным выводом.
Важный показатель здесь — целостность.
Это не была малозначимая ошибка метаданных. Атакующий полностью контролировал байты замены, записанные в путь вложения другой страницы, и страница жертвы продолжала отдавать изменённый объект после этого.
Это именно тот вид сохранённого межзаписного вмешательства, который заслуживает Высокой целостности.
Доступность остаётся низкой, что также имеет смысл: повреждение диаграммы или прикреплённого документа может сделать контент жертвы непригодным для использования, но основное воздействие — это всё же несанкционированное изменение, а не полное нарушение обслуживания.
Я сообщил о проблеме в частном порядке через GitHub Security Advisories, предоставив:
Проблема была принята мейнтейнером, назначена CVE-2026-34213 и опубликована 14 апреля 2026 года.
Публичное уведомление содержит:
>= v0.3.0v0.71.0Эта история также совпала с моим локальным обзором кода: уязвимая логика перезаписи присутствовала в самой ранней помеченной версии, которую я проверил в уязвимой строке.
Интересный урок здесь не просто «используйте || вместо &&».
Это симптом.
Глубинный урок:
если одно поле, контролируемое пользователем, подтверждает авторизацию, а другое поле, контролируемое пользователем, выбирает обновляемый объект, эти два поля должны быть явно и точно связаны вместе.
Это правило проявляется повсюду:
Как только система говорит:
она создала границу безопасности, которая должна обеспечиваться инвариантами точного совпадения.
Всё, что мягче этого, рано или поздно превращается в ошибку с контролируемым пользователем ключом.
Эта проблема также подкрепляет второй момент, который легко недооценить:
небольшие логические ошибки в защитном коде могут иметь первостепенные последствия для безопасности.
Условие из трёх пунктов, которое «выглядит разумно» на первый взгляд, было достаточно, чтобы инвертировать модель защиты для пути перезаписи.
Вот почему эти поверхности заслуживают тщательного анализа, а не поверхностной уверенности.
pageId, но выбор цели перезаписи использовал отдельный указанный вызывающим attachmentId.v0.71.0 правильно изменило защиту на отклонение при любом несовпадении.Эта уязвимость была не об экзотическом поведении хранилища.
Она была о том, что путь обновления доверял выбранному атакующим идентификатору объекта больше, чем следовало.
Docmost подтверждал доступ на редактирование одной страницы, принимал существующий идентификатор вложения с другой страницы, а затем позволял ошибочной проверке перезаписи превратить это несовпадение в успешную межстраничную замену файла.
Вот почему она стала CVE-2026-34213.
Патч в v0.71.0 чисто исправил непосредственную проблему, но более широкий урок остаётся ценным:
когда авторизация и выбор объекта разделены по разным контролируемым пользователем полям, точная привязка является свойством безопасности.