
Публичная ссылка выглядела чистой в дереве страниц, но конечная точка поиска рассказала другую историю. В Docmost скрытые от просмотра по публичной ссылке дочерние страницы с ограниченным доступом могли всё ещё просачиваться через результаты поиска по публичной ссылке.
Публичная ссылка выглядела чистой в дереве страниц, но поисковая конечная точка рассказывала другую историю. В Docmost скрытые дочерние страницы, недоступные для просмотра через публичную ссылку, могли всё равно просачиваться через результаты поиска по публичной ссылке.
Я обнаружил эту проблему, изучая Docmost — платформу с открытым исходным кодом для совместных вики и документации, — задавшись очень простым вопросом:
Если страница намеренно скрыта от публичного зрителя, каждый ли публичный функционал соблюдает то же ограничение?
В данном случае ответ был отрицательным.
Ограниченная дочерняя страница могла оставаться скрытой в дереве публичной ссылки, но при этом просачиваться через конечную точку поиска по публичной ссылке.
Проблема была принята и получила идентификатор CVE-2026-33146.
Docmost: Docmost на GitHub
CVE: CVE-2026-33146
Официальный сайт Docmost представляет его как корпоративную вики, развёртываемую на месте, с более 3 миллионов загрузок, и утверждает, что ему доверяют команды таких организаций, как Vilnius City, Bechtle, Правительство Австралии, Красный Крест и ETS Quebec.
публичная родительская ссылка с включенными подстраницами → ограниченный потомок исключён из публичного дерева → злоумышленник запрашивает поиск по публичной ссылке → утекают заголовок и фрагмент ограниченного потомка
Docmost — это платформа для совместных вики и документации.
Она предоставляет:
Это означает, что её модель публичного доступа является реальной границей безопасности.
Важный вопрос заключался не в том, может ли Docmost публично делиться страницами.
Настоящий вопрос заключался в следующем:
Когда Docmost решает, что дочерняя страница является ограниченной и не должна отображаться посетителю по публичной ссылке, соблюдается ли это ограничение везде в потоке публичного доступа?
В данном случае — нет.
Многие проверки безопасности прекращаются слишком рано, когда видят страницу, скрытую в интерфейсе.
Этого недостаточно.
Более сильный вопрос звучит так:
Каждый ли серверный путь применяет одно и то же решение о видимости?
Это важно, потому что границы безопасности определяются не тем, как выглядит интерфейс.
Они определяются тем, что на самом деле возвращает сервер.
Здесь конечная точка дерева публичных ссылок вела себя безопасно:
Но путь поиска по публичной ссылке вёл себя иначе:
Это превратило проблему в реальную уязвимость авторизации и раскрытия информации, а не просто в несоответствие отображения.
Я не подходил к Docmost, случайно фаззя конечные точки в надежде, что появится что-то интересное.
Более сильный путь — сначала выбрать доверенную границу.
Для приложений, поддерживающих:
один из лучших вопросов:
Применяет ли уровень поиска точно такую же границу авторизации, как и уровень просмотра?
Этот вопрос становится особенно ценным, когда:
Именно здесь и проявилась эта проблема.
Ошибка была не в том, что Docmost не смог скрыть ограниченную страницу в обычном публичном дереве.
Ошибка заключалась в том, что публичный поиск не учитывал ту же логику ограничений.
Из анализа исходного кода: поток публичного дерева использовал обход потомков с учётом ограничений.
Соответствующая область:
apps/server/src/core/share/share.service.tsЭтот путь намеренно исключал ограниченных потомков, используя:
getPageAndDescendantsExcludingRestricted(...)Но поток поиска по публичной ссылке шёл по другому пути.
Соответствующие области:
apps/server/src/core/search/search.controller.tsapps/server/src/core/search/search.service.tsТам код собирал потомков, используя:
getPageAndDescendants(...)Это означало, что ограниченные потомки оставались в области поиска.
В контексте публичного доступа это имеет большое значение, потому что ветка поиска выполняется без обычного контекста разрешений аутентифицированного пользователя. Как только ограниченные потомки были включены в набор страниц, доступных для поиска, их метаданные могли просачиваться через ответ.
Потому что злоумышленнику не нужна аутентифицированная учётная запись.
Ему нужно только:
Как только это условие выполнено, публичный посетитель может запросить конечную точку поиска по общей ссылке и восстановить:
Этого достаточно для создания утечки конфиденциальности, даже если полное тело страницы не возвращается.
Важное различие в том, что приложение уже чётко сигнализирует о предполагаемой модели безопасности.
Конечная точка публичного дерева скрывает ограниченных потомков.
Поэтому настоящий вопрос не:
«Случайно ли поиск возвращает более широкий набор результатов?»
Настоящий вопрос:
«Нарушает ли поиск решение об авторизации, уже принятое в другом месте для той же границы публичного доступа?»
В Docmost — да.
Это превращает проблему из:
в:
Вот почему это настоящая уязвимость.
Я подтвердил проблему, сравнив две соответствующие публичные конечные точки бок о бок.
Сначала я проверил обычную конечную точку публичного дерева, используя ключ публичной ссылки.
Пример запроса:
POST /api/shares/tree HTTP/1.1
Host: 127.0.0.1:6752
Content-Type: application/json
{
"shareId": "public-share-key"
}
Ответ вернул только публичную дочернюю страницу в дереве страниц.
Показательный результат:
{
"pageTree": [
{
"id": "public-child",
"title": "Public roadmap"
}
]
}
Это установило ожидаемое поведение продукта:
Затем я запросил конечную точку поиска по публичной ссылке, используя термин, который встречался внутри ограниченного потомка.
Пример запроса:
POST /api/search/share-search HTTP/1.1
Host: 127.0.0.1:6752
Content-Type: application/json
{
"shareId": "public-share-key",
"query": "salary"
}
Ответ всё равно включил ограниченного потомка:
{
"items": [
{
"id": "public-child",
"title": "Public roadmap",
"highlight": "release plan and milestones"
},
{
"id": "restricted-child",
"title": "Payroll Q4",
"highlight": "salary bands and bonus targets"
}
]
}
Это доказало основное утверждение:
Самая сильная сторона этой проблемы — не второй запрос сам по себе.
Это контраст между двумя конечными точками.
Показывает, что продукт уже имеет предполагаемую модель ограничений для публичных ссылок.
Ограниченный потомок не должен быть виден публичному посетителю.
Доказывает, что путь поиска нарушает ту же самую границу.
Это затрудняет отклонение проблемы как ожидаемого поведения поиска или пробела в документации.
Само приложение устанавливает правило через ответ дерева, а затем нарушает его через ответ поиска.
Это веское доказательство.
Эта проблема не раскрывает произвольный контент во всём рабочем пространстве.
Её масштаб уже.
Но в пределах затрагиваемого поддерева публичной ссылки она всё же даёт злоумышленнику полезные несанкционированные знания:
Даже короткие фрагменты могут иметь значение.
Заголовок типа:
уже создаёт ценность для злоумышленника.
Поэтому, хотя в итоге проблема была классифицирована как Умеренная, это всё же действительная проблема конфиденциальности с чётким и защищаемым нарушением границы.
Этой проблеме был присвоен:
Уровень серьёзности в уведомлении:
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:L/I:N/A:NЭта оценка отражает более узкую утечку конфиденциальности, а не полный несанкционированный доступ к документам.
Важно то, что проблема всё ещё действительна.
Утверждение здесь не:
Утверждение следующее:
Это настоящее раскрытие информации, связанное с авторизацией.
Некоторые люди слишком быстро отвергают утечки метаданных.
Это ошибка.
Настоящий вопрос — пересекают ли утёкшие данные намеченную границу.
Здесь — пересекли.
Если приложение говорит:
но публичная конечная точка всё равно раскрывает:
то модель конфиденциальности нарушена, даже если влияние ограничено.
Это делает ошибку достойной сообщения.
Чистые, ограниченные и воспроизводимые ошибки, подобные этой, — именно те проблемы, которые помогают продемонстрировать здравые суждения при проверке безопасности.
Самый безопасный путь исправления — заставить публичный поиск использовать ту же логику обхода потомков с учётом ограничений, что и поток публичного дерева.
На практике это означает, что ветка поиска по общей ссылке не должна перечислять потомков с помощью:
getPageAndDescendants(...)
Вместо этого она должна соответствовать более безопасному обходу публичной ссылки и использовать:
getPageAndDescendantsExcludingRestricted(...)
Альтернативное исправление — сохранить более широкое перечисление, а затем явно отфильтровать ограниченных потомков перед тем, как поиск вернёт результаты.
Но более чистый дизайн прост:
граница поиска должна совпадать с границей просмотра
Именно это свойство безопасности было нарушено.
Об этой проблеме было сообщено приватно через поток безопасности GitHub.
Отчёт показывал:
/api/shares/tree/api/search/share-searchПроблема была принята и получила:
CVE-2026-33146
Итоговый уровень серьёзности уведомления — Умеренный, что лучше соответствует более узкому объёму утечки, чем более широкая критичность.
Это не ослабляет обоснованность находки.
Это просто более точно определяет её влияние.
Ключевой урок прост:
скрыть что-то в одной публичной конечной точке недостаточно, если другая публичная конечная точка всё равно это раскрывает.
Многие разработчики думают об авторизации только в очевидном пути отображения:
Но реальная граница шире.
Также нужно спросить:
В Docmost ответ был отрицательным.
Вот в чём суть.
Эта уязвимость не была связана с эффектными полезными нагрузками или сложными цепочками эксплойтов.
Она заключалась в задании очень практического вопроса о доверенной границе.
Docmost скрыл ограниченную страницу в одном месте.
Затем раскрыл её в другом.
Вот почему это стало CVE-2026-33146.