公開共有はページツリー上ではきれいに見えましたが、検索エンドポイントは別の話をしていました。Docmost では、公開共有ビューアから隠された制限付き子ページが、公開共有検索結果を通じて漏洩する可能性がありました。
この問題は、オープンソースのコラボレーティブウィキおよびドキュメンテーションプラットフォームである Docmost のレビュー中に、非常にシンプルな疑問から見つけました。
公開共有ビューアから意図的に隠されたページがある場合、すべての公開機能はその制限の境界を同じように尊重するのか?
このケースでは、答えは「いいえ」でした。
制限付き子ページは公開共有ツリー内では非表示のままでいられる一方、公開共有検索エンドポイントを通じて漏洩する可能性がありました。
この問題は受理され、CVE-2026-33146 が割り当てられました。
Docmost: GitHub 上の Docmost
CVE: CVE-2026-33146
Docmost の公式サイトでは、エンタープライズ対応のオンプレミスウィキとして紹介されており、300 万回以上のダウンロード があり、ビリニュス市、Bechtle、オーストラリア政府、赤十字社、ETS Quebec などの組織のチームから信頼されています。
サブページが有効になった公開親共有 → 公開ツリーから省略された制限付き子孫 → 攻撃者が公開共有検索をクエリ → 制限付き子ページのタイトルとスニペットが漏洩
Docmost はコラボレーティブなウィキおよびドキュメンテーションプラットフォームです。
次の機能を提供します:
つまり、その公開共有モデルは実際のセキュリティ境界です。
ここで重要な疑問は、Docmost がページを公開共有できるかどうかではありません。
本当の疑問は次の通りです:
Docmost が子孫ページを制限付きと判断し、公開共有訪問者に表示すべきでないと決定した場合、その制限は公開共有フローのすべての場所で守られるのか?
このケースでは、守られませんでした。
多くのセキュリティレビューは、ページが UI で非表示になっているのを見た時点で早々に終了してしまいます。
それでは不十分です。
より強力な疑問は次の通りです:
すべてのバックエンドパスが同じ可視性の決定を強制しているのか?
なぜなら、セキュリティ境界はインターフェースの見た目で定義されるのではなく、サーバーが実際に返すものによって定義されるからです。
ここでは、公開ツリーエンドポイントは安全でした:
しかし、公開共有検索パスは異なる動作をしました:
これにより、これは単なる表示の不一致ではなく、実際の認可および情報漏洩の問題となりました。
私は、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 では、その通りでした。
これにより、これは:
ではなく、
となります。
それが、これが実際の脆弱性である理由です。
私は、関連する2つの公開エンドポイントを並べて比較することで問題を検証しました。
最初に、公開共有キーを使用して通常の公開ツリーエンドポイントをテストしました。
リクエスト例:
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"
}
]
}
これにより、核心的な主張が証明されました:
この問題の最も強力な部分は、2番目のリクエスト単独ではありません。
それは、2つのエンドポイントの対比です。
製品が公開共有に対してすでに意図された制限モデルを持っていることが示されています。
制限付き子孫は公開訪問者に表示されるべきではありません。
検索パスがまさにその同じ境界を壊していることが証明されています。
これにより、この問題を予想される検索動作やドキュメントのギャップとして片付けることが難しくなります。
アプリケーション自体がツリー応答を通じてルールを確立し、その後検索応答を通じてそれを破っています。
これは強力な証拠です。
この問題は、ワークスペース全体の任意のコンテンツを公開するわけではありません。
その範囲はそれより狭いです。
しかし、影響を受ける公開共有サブツリー内では、攻撃者に依然として有用な無認可の知識を与えます:
短いスニペットでも重要になり得ます。
次のようなタイトル:
は、すでに攻撃者にとってセキュリティ上の価値を生み出します。
そのため、最終的に Moderate と分類されたものの、これは依然として有効な機密性の問題であり、明確で防御可能な境界違反があります。
この問題には次のものが割り当てられました:
アドバイザリの深刻度は:
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 が割り当てられました:
CVE-2026-33146
最終的なアドバイザリの深刻度は Moderate であり、より広範な重大性を主張するよりも、より狭い漏洩範囲に適しています。
これは発見の有効性を弱めるものではありません。
単にその影響をより正確に定義するものです。
ここでの主要な教訓は単純です:
1 つの公開エンドポイントで何かを非表示にしても、別の公開エンドポイントがそれを依然として明らかにするのであれば十分ではない。
多くの開発者は、認可を明白なレンダリングパスでのみ考えます:
しかし、実際の境界はより広いです。
次のことも問いかける必要があります:
Docmost では、答えは「いいえ」でした。
それが本当の教訓です。
この脆弱性は派手なペイロードや複雑なエクスプロイトチェーンに関するものではありませんでした。
非常に実用的な信頼境界の質問をすることに関するものでした。
Docmost は制限付きページをある場所では非表示にしました。
そして別の場所で漏洩させました。
それが、これが CVE-2026-33146 となった理由です。