Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2026-34212 — Docmost принял javascript: URL внутри узла вложения, сохранил его при хранении и рендеринге и превратил обратно в кликабельную ссылку в источнике Docmost. | Kitploit
Инструменты/GitHubGitHub/0xmrma/cve-2026-34212
Анализ уязвимостейЭксплуатацияВеб-безопасностьТестирование на ПроникновениеСтатьи и ИсследованияОбучение и Образование
GitHub0xmrma/cve-2026-34212

CVE-2026-34212

Docmost принял javascript: URL внутри узла вложения, сохранил его при хранении и рендеринге и превратил обратно в кликабельную ссылку в источнике Docmost.

Репозиторий
12 месяцев назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2026-34212

Docmost принимал URL с javascript: внутри узла вложения, сохранял его при хранении и рендеринге, и превращал обратно в кликабельную ссылку в источнике Docmost.

Введение

Я выявил, ответственно раскрыл и воспроизвел уязвимость хранимого XSS высокой степени серьезности в Docmost — платформе совместной документации с открытым исходным кодом.

Официальный сайт Docmost представляет её как корпоративную вики для развертывания на собственных серверах с 3 млн+ загрузок, и утверждает, что ей доверяют команды таких организаций, как Vilnius City, Bechtle, Правительство Австралии, Красный Крест и ETS Quebec.

Ошибка находилась в месте, которое легко упустить в системах с форматированным текстом:

не в обычном расширении для ссылок, а в отдельном пользовательском типе узла, используемом для вложений.

Я проверял конвейер редактора с очень конкретным вопросом:

если обычные ссылки блокируют URL с javascript:, применяют ли узлы вложений то же правило, прежде чем попасть в опасный приемник ссылки (anchor)?

В уязвимых версиях — нет.

Docmost принимал вредоносный узел вложения в JSON страницы, сохранял его атрибут url без изменений, а затем рендерил это значение обратно в кликабельный элемент <a href="javascript:...">.

Эта проблема стала CVE-2026-34212.

Docmost: docmost/docmost
Уведомление: GHSA-cf68-cff9-hq4w
CVE: CVE-2026-34212
Исправлено в: v0.71.0

photo0

Цепочка атаки

URL узла вложения под контролем атакующего -> JSON страницы принимается и сохраняется без изменений -> рендеринг HTML/React превращает этот URL в href ссылки -> жертва нажимает на действие с вложением -> управляемый атакующим JavaScript выполняется в источнике Docmost


Что делает эта часть Docmost

Docmost хранит содержимое страницы в формате JSON, совместимом с ProseMirror/Tiptap.

Эта модель содержимого включает пользовательские блочные узлы для таких вещей, как:

  • изображения
  • диаграммы
  • встраивания
  • вложения

Узел вложения хранит такие поля, как:

  • url
  • name
  • mime
  • size
  • attachmentId

Сервер принимает содержимое страницы в нескольких форматах:

  • json
  • markdown
  • html

и нормализует его в ProseMirror JSON перед сохранением.

Это означает, что любой тип узла, который может содержать URL, находится на прямой границе доверия.

Если один из этих типов узлов в конечном итоге рендерится в <a href>, обработка схемы URL не является опциональной. Это часть модели безопасности.


Почему эта поверхность стоила внимания

Пользовательские расширения редактора часто являются источником дрейфа безопасности.

Базовая система, возможно, уже знает, как правильно обрабатывать опасные URL, но каждый пользовательский узел все равно должен повторно применять те же правила в своих собственных приемниках.

Это создает предсказуемую стратегию проверки:

  • найти каждый тип узла, который хранит URL-подобное поле
  • проследить, где это поле принимается
  • проследить, где это поле рендерится
  • сравнить его поведение по очистке с обычной обработкой ссылок платформы

Именно так и была обнаружена эта ошибка.

Обычное расширение ссылок Docmost уже считало javascript: опасным.

Узел вложения — нет.

Как только вы видите эту асимметрию, вопрос безопасности становится очевидным:

могу ли я сохранить узел вложения, чей url равен javascript:, и получить его обратно в виде живой ссылки?

Ответ был — да.


Первопричина

Первопричиной была непоследовательная очистка URL между типами узлов содержимого.

Серверный путь для содержимого принимал произвольные URL вложений, пока в целом содержимое соответствовало схеме ProseMirror.

В уязвимой версии:

  • CreatePageDto принимал content?: string | object
  • PageService.parseProsemirrorContent() нормализовал markdown, html или json
  • затем сервер вызывал jsonToNode(prosemirrorJson)
  • если валидация схемы проходила, содержимое сохранялось

Этот этап валидации проверял структурную корректность, а не безопасность URL.

Критическая часть уязвимой серверной логики фактически выглядела так:

root@kitploit:~
prosemirrorJson = content;
jsonToNode(prosemirrorJson);
return prosemirrorJson;

Никакой нормализации схемы URL вложений там не происходило.

Позже расширение вложения рендерило значение, контролируемое атакующим, напрямую.

Уязвимый узел вложения делал следующее:

root@kitploit:~
url: {
  default: "",
  parseHTML: (element) => element.getAttribute("data-attachment-url"),
  renderHTML: (attributes) => ({
    "data-attachment-url": attributes.url,
  }),
},

а затем:

root@kitploit:~
[
  "a",
  {
    href: HTMLAttributes["data-attachment-url"],
    class: "attachment",
    target: "blank",
  },
  `${HTMLAttributes["data-attachment-name"]}`,
]

На клиентской стороне представление узла React снова оборачивало это в:

root@kitploit:~
<a href={getFileUrl(url)} target="_blank">

Но getFileUrl() обрабатывал только особые случаи:

  • абсолютные http URL
  • /api/...
  • /files/...

Всё остальное возвращалось без изменений.

Так что полезная нагрузка вида:

root@kitploit:~
javascript:alert(document.domain)

выживала:

  • хранение в JSON
  • серверная валидация схемы
  • рендеринг HTML
  • клиентская обработка URL

Уже этого было бы достаточно для хранимого XSS.

Что делает первопричину особенно ясной — это точка сравнения.

Обычное расширение ссылок Docmost явно блокировало javascript::

  • оно отклоняло javascript: в parseHTML()
  • оно обнуляло href с javascript: в renderHTML()

Таким образом, продукт уже знал, что эта схема опасна.

Узел вложения просто не применял ту же политику.

Вот почему это был не «общий XSS в редакторе».

Это был пробел на границе доверия, специфичный для узла.


Почему это проблема безопасности, а не просто отсутствие очистки

Эта ошибка была не просто о небезопасной эстетике HTML.

Она позволяла атакующему, который мог редактировать страницу, сохранить вредоносную полезную нагрузку, которая позже выполнялась бы в источнике Docmost, когда другой пользователь взаимодействовал с отрендеренным вложением.

Это важно, потому что скрипт внутри источника может:

  • читать данные, доступные жертве
  • отправлять аутентифицированные запросы от имени жертвы
  • изменять содержимое, которое жертва имеет право изменять
  • злоупотреблять любым DOM или API-интерфейсом, доступным сессии

Требование клика не сводит это к тривиальной проблеме.

Клик является частью нормального поведения продукта: интерфейс намеренно представляет вложение как интерактивную ссылку/иконку.

Поэтому вопрос безопасности не в том, «может ли атакующий заставить произвольный JS без какого-либо взаимодействия?»

Реальный вопрос:

хранит ли приложение скриптоносное содержимое под контролем атакующего и затем представляет его другим пользователям как доверенный путь взаимодействия?

В уязвимых версиях — да.

Это хранимый XSS.


Почему эксплуатация была практичной

Путь эксплуатации был прямолинейным:

  • любой пользователь с правами редактирования страницы мог разместить полезную нагрузку
  • вредоносный URL сохранялся без изменений
  • страница рендерилась нормально
  • просматривающим нужен был только стандартный доступ к странице
  • одного клика по действию с вложением было достаточно для запуска выполнения

Это также делало пользователей с более высокими привилегиями реалистичными целями.

Если владелец рабочего пространства, администратор или широко доверенный редактор просматривал контент, контролируемый атакующим, и кликал по действию с вложением, скрипт атакующего запускался в контексте более привилегированной сессии.

Это важный практический момент:

требования к привилегиям атакующего были только низкими. Уровень привилегий жертвы определял ценность сессии XSS.


Подтверждение концепции

Я подтвердил проблему вживую на Docmost v0.70.3.

PoC использовал только обычные HTTP-запросы и собственные API страниц приложения.

Последовательность была:

  1. Войти как пользователь, который может редактировать страницу.
  2. Создать или выбрать страницу.
  3. Отправить POST /api/pages/update с format: "json" и узлом вложения, чей url — полезная нагрузка с javascript:.
  4. Запросить страницу обратно через POST /api/pages/info.
  5. Подтвердить, что сохраненный JSON все еще содержит вредоносный URL.
  6. Запросить ту же страницу в формате HTML и подтвердить, что сервер возвращает ссылку, чей href все еще javascript:....
  7. В интерфейсе просматривающий, кликнув по отрендеренному действию вложения, выполняет полезную нагрузку в источнике Docmost.

Минимальное вредоносное содержимое было:

root@kitploit:~
{
  "pageId": "<pageId>",
  "content": {
    "type": "doc",
    "content": [
      {
        "type": "attachment",
        "attrs": {
          "url": "javascript:alert(document.domain)",
          "name": "policy.pdf",
          "mime": "application/pdf",
          "size": 1
        }
      }
    ]
  },
  "operation": "replace",
  "format": "json"
}

Наблюдаемый живой результат моего теста:

  • API принял вредоносный узел вложения без изменений
  • сохраненный ID страницы был 019d18cf-4212-70b0-894a-fe20080fb0f1
  • POST /api/pages/info вернул сохраненный JSON с:
root@kitploit:~
"url": "javascript:alert(document.domain)"
  • POST /api/pages/info с format: "html" вернул HTML, содержащий:
root@kitploit:~
<div data-type="attachment" data-attachment-url="javascript:alert(document.domain)" data-attachment-name="policy.pdf" data-attachment-mime="application/pdf" data-attachment-size="1"><a href="javascript:alert(document.domain)" class="attachment" target="blank">policy.pdf</a></div>

Этот HTML-ответ является критическим доказательством.

Мне не нужно было опираться на расплывчатое утверждение, что «браузер может сделать что-то интересное».

Приложение само отрендерило точный исполняемый приемник.

Как только пользователь кликает по ссылке/иконке вложения, браузер выполняет URL javascript: в источнике страницы, которая его создала.


Почему PoC был выбран таким образом

Для XSS, управляемого редактором, одних скриншотов недостаточно.

Они показывают симптомы, а не сбой на границе.

Вот почему я структурировал PoC вокруг двух явных контрольных точек:

  1. доказательство хранения
  2. доказательство отрендеренного приемника

Доказательство хранения показало, что сервер принял и сохранил опасную схему.

Доказательство отрендеренного приемника показало, что приложение превратило это сохраненное значение обратно в:

root@kitploit:~
<a href="javascript:...">

Это разделение имеет значение.

Если продукт сохраняет опасный ввод, но нейтрализует его перед каждым приемником, у вас может быть пробел в усилении защиты, но не обязательно живой XSS.

Если продукт сохраняет опасный ввод и затем рендерит его в реальный исполняемый приемник, у вас есть полная цепочка уязвимости.

Именно это и произошло здесь.


Анализ исправления

Исправление было выпущено в v0.71.0 и устранило путь эксплуатации путем применения очистки URL к URL вложений.

Расширение вложения теперь импортирует и использует sanitizeUrl, включая:

  • очистку data-attachment-url во время разбора
  • очистку data-attachment-url во время рендеринга
  • очистку href ссылки

Концептуально, патч изменил узел вложения с:

  • доверять сырой URL вложения
  • выводить сырой URL вложения

на:

  • нормализовать URL вложения, прежде чем он станет частью отрендеренного узла

Клиентский хелпер getFileUrl() также был обновлен, так что неизвестные схемы больше не проходят без изменений. В исправленной версии путь по умолчанию возвращает sanitizeUrl(src) вместо возврата src как есть.

Это важная часть исправления, потому что уязвимый дизайн имел две усиливающие проблемы:

  • узел рендерил сырой href
  • клиентский fallback считал неизвестные схемы приемлемыми

Патч удалил оба предположения.

Это было хорошее исправление для живого пути XSS, потому что оно привело обработку URL вложений в соответствие с остальной моделью безопасности редактора.

Тем не менее, есть еще более широкий урок по усилению защиты:

очистка на клиенте или во время рендеринга здесь необходима, но серверное отклонение опасных схем во время создания/обновления страницы было бы еще более сильным инвариантом.

Самая безопасная долгосрочная модель:

  • отклонять очевидно опасные схемы при приеме
  • очищать снова на границах рендеринга

Глубокоэшелонированная защита имеет значение в системах с форматированным содержимым.


Важные случаи регрессии

Для долгосрочного покрытия наиболее важны следующие случаи:

  • обновления страниц в JSON, содержащие attachment.attrs.url = "javascript:..."
  • импорт HTML, содержащий data-attachment-url="javascript:..."
  • рендеринг вложения никогда не должен выдавать href="javascript:..."
  • клиентские вспомогательные функции не должны возвращать неизвестные исполняемые схемы без изменений
  • узлы вложений и обычные узлы ссылок должны разделять эквивалентную политику в отношении схем URL
  • безопасные внутренние пути вложений, такие как /api/files/... и /files/..., должны продолжать работать нормально

Ключевой момент — согласованность.

Если обычные ссылки очищаются, а пользовательские узлы, несущие URL, — нет, то у редактора на самом деле нет единой политики безопасности URL.

У него есть фрагменты, а фрагменты — это то место, где живут ошибки XSS.


Серьезность и классификация

Опубликованное уведомление классифицировало эту проблему как:

  • CWE-79: Некорректная нейтрализация входных данных во время генерации веб-страницы
  • CVSS v3.1:
root@kitploit:~
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N

Это дает 7.6 / Высокий.

Это обоснованная классификация.

Важные свойства:

  • низкое требование к привилегиям атакующего
  • сохраненная полезная нагрузка
  • выполнение в источнике Docmost
  • изменение области воздействия
  • значительное влияние на конфиденциальность, поскольку скрипт может получить доступ к видимым в приложении данным жертвы

Взаимодействие с пользователем по-прежнему требуется, потому что жертва должна активировать ссылку/иконку вложения. Вот почему UI:R правилен.

Но как только это взаимодействие происходит, граница безопасности уже была нарушена гораздо раньше: приложение сохранило опасную схему и отрендерило её обратно в исполняемый приемник.


Раскрытие

Я сообщил о проблеме приватно через GitHub Security Advisories с:

  • анализом первопричины
  • живым HTTP PoC
  • доказательством сохраненного JSON
  • доказательством отрендеренного HTML приемника
  • закрепленным одноразовым тестовым стендом

Проблема была принята, ей присвоен CVE-2026-34212, и она была опубликована 14 апреля 2026 года.

Публичное уведомление в настоящее время указывает:

  • уязвимая версия: 0.70.3
  • исправленная версия: 0.71.0

Мое живое подтверждение было выполнено на v0.70.3, что совпадает с опубликованной уязвимой версией.


Чему на самом деле учит эта ошибка

Главный урок здесь не просто «очищать URL».

Это и так все знают.

Более интересный урок:

Если в приложении есть один безопасный тип узла, несущего URL, и один небезопасный, то небезопасный и есть реальная политика.

Системы с форматированным текстом часто накапливают пользовательские расширения быстрее, чем накапливают проверку безопасности.

Это создает именно такую асимметрию:

  • стандартный путь для ссылок укреплен
  • путь для вложений рассматривается как «внутренний» или «особый»
  • особый путь незаметно становится более легким приемником XSS

Эта ошибка также показывает, почему валидации схемы недостаточно.

jsonToNode() проверял, что содержимое структурно является корректными данными ProseMirror. Он не доказывал, что содержимое безопасно для рендеринга.

Это разные вопросы.

Проверка безопасности становится намного острее, если разделять эти вопросы:

  • структурно ли это содержимое корректно?
  • безопасно ли это содержимое для хранения?
  • безопасно ли это содержимое для рендеринга в каждом приемнике?

Узел вложения прошел первый вопрос и провалил третий.

Вот как ошибки с сохраненным содержимым выживают внутри в остальном хорошо структурированных конвейеров редакторов.


Ключевые моменты

  • Docmost принимал сырые URL узлов вложений в содержимом страницы.
  • Серверная валидация страницы проверяла форму схемы ProseMirror, а не безопасность схемы URL.
  • Уязвимый узел вложения рендерил data-attachment-url и href ссылки напрямую из ввода, контролируемого атакующим.
  • Клиентский хелпер getFileUrl() возвращал неизвестные схемы без изменений.
  • Обычные узлы ссылок уже блокировали javascript:, а узлы вложений — нет.
  • Редактор с низкими привилегиями мог единожды разместить полезную нагрузку и целиться в последующих просматривающих.
  • Живой PoC доказал как сохранение, так и отрендеренный исполняемый приемник.
  • Исправление в v0.71.0 добавило обработку sanitizeUrl для узла вложений и клиентского пути fallback.

Заключительные слова

Эта уязвимость была не о странности браузера.

Она была о пользовательском узле содержимого, который обошел собственные предположения безопасности URL приложения.

Docmost принял URL вложения, контролируемый атакующим, сохранил его, а затем отрендерил обратно в живую ссылку в источнике приложения.

Вот почему она стала CVE-2026-34212.

Патч в v0.71.0 чисто закрыл активный путь XSS, но более широкий урок стоит запомнить:

в приложениях с тяжелым редактором каждый пользовательский узел, который может нести URL, является собственной границей безопасности, и его нужно проверять как таковой.

Скачать инструмент