一个公开分享在页面树中看起来是干净的,但搜索端点却讲述了不同的故事。在 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 中,是的。
这就把这从一个问题:
变成了:
这就是为什么这是一个真正的漏洞。
我通过并排比较两个相关的公开端点来验证该问题。
首先,我使用公开分享密钥测试了正常的公开树端点。
示例请求:
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该评分反映的是较窄的机密性泄露,而非完全的未经授权文档访问。
重要的一点是,该问题仍然是有效的。
这里的主张并非:
而是:
这是一个真正的与授权相关的信息泄露。
有些人太快地忽略了元数据泄露。
这是个错误。
真正的问题是泄露的数据是否越过了预期的边界。
在这里,是的。
如果应用程序说:
但一个公开端点仍然泄露了:
那么机密性模型已经失败,即使影响有限。
这使得它值得报告。
像这样干净、范围明确且可复现的 bug 正是那种有助于展示强安全审查判断的问题。
最安全的修复方向是让公开搜索使用与公开树流程相同的受限感知后代逻辑。
在实践中,这意味着分享搜索分支不应使用以下方式枚举后代:
getPageAndDescendants(...)
而应调整为更安全的公开分享遍历方法,使用:
getPageAndDescendantsExcludingRestricted(...)
另一种修复方式是保持更广泛的枚举,然后在搜索查询返回结果之前显式过滤掉受限后代。
但更干净的设计很简单:
搜索边界应与浏览边界匹配
这正是失败的安全属性。
该问题通过 GitHub 的安全报告流程私下报告。
报告显示了:
/api/shares/tree 的预期安全行为/api/search/share-search 的不一致脆弱行为该问题已被接受并分配了:
CVE-2026-33146
最终安全公告的严重性为 中等,这比更广泛的严重性声明更符合较窄的泄露范围。
这并不会削弱该发现的有效性。
它只是更精确地定义了其影响。
这里的关键教训很简单:
在一个公开端点中隐藏某物是不够的,如果另一个公开端点仍然泄露它。
许多开发者只考虑明显渲染路径中的授权:
但真正的边界要更广。
你还必须问:
在 Docmost 中,答案是否定的。
这才是真正的要点。
这个漏洞并非关于花哨的 payload 或复杂的利用链。
而是关于提出一个非常实际的信任边界问题。
Docmost 在一个地方隐藏了受限页面。
然后在另一个地方泄露了它。
这就是为什么这成为了 CVE-2026-33146。