0xmrma/CVE-2026-33146
GitHub: 0xmrma/CVE-2026-33146
剖析 Docmost 协作平台中受限子页面通过公开搜索端点泄露标题与内容片段的授权绕过漏洞(CVE-2026-33146),含完整 PoC 与源码级根因分析。
Stars: 0 | Forks: 0
# CVE-2026-33146
在页面树中,一个公开分享看起来很干净,但搜索端点却讲述了不同的故事。在 Docmost 中,对公开分享浏览者隐藏的受限子页面仍然可以通过公开分享的搜索结果泄露出来。
## 简介
我在审查 **Docmost**(一个开源的协作 wiki 和文档平台)时发现了这个问题,当时我脑海中有一个非常简单的问题:
**如果一个页面被特意对公开分享浏览者隐藏,是否所有的公开功能都会遵守相同的限制边界?**
在这种情况下,答案是否定的。
一个受限的子页面可以在公开分享树中保持隐藏,但仍然通过公开分享的搜索端点泄露出去。
该问题已被接受并被分配了 **CVE-2026-33146**。
**Docmost:** [GitHub 上的 Docmost](https://github.com/docmost/docmost)
**CVE:** CVE-2026-33146
## 攻击链
`启用了子页面的公开父分享 → 受限后代在公开树中被省略 → 攻击者查询公开分享搜索 → 受限子页面的标题和片段泄露`
## Docmost 的功能
**Docmost** 是一个协作 wiki 和文档平台。
它提供:
- 共享页面
- 公开分享链接
- 嵌套的页面树
- 工作空间和空间级的内容组织
- 跨共享内容的搜索
这意味着其公开分享模型是一个真正的安全边界。
这里的重要问题不是 Docmost 能否公开分享页面。
真正的问题是:
**当 Docmost 决定一个后代页面是受限的,并且不应该向公开分享访客展示时,这种限制是否在公开分享流程的各个环节都成立?**
在这种情况下,它并没有。
## 为什么这个 Bug 值得关注
许多安全审查在看到页面在 UI 中被隐藏后就过早地停止了。
这还不够。
更有力的问题是:
**是否每一个后端路径都执行了相同的可见性决策?**
这很重要,因为安全边界不是由界面的外观定义的。
它们是由服务器实际返回的内容定义的。
在这里,公开树端点的行为是安全的:
- 受限的后代被隐藏了
但是公开分享搜索路径的行为却不同:
- 受限的后代仍然影响了搜索结果
- 它们的标题泄露了
- 它们高亮的内容片段泄露了
这使得这是一个真正的授权和信息泄露问题,而不仅仅是一个展示上的不一致。
## 我关注的边界
我并没有通过随机模糊测试路由并期望出现有趣的结果来接触 Docmost。
更强有力的方法是首先选择一个信任边界。
- 公开分享
- 嵌套对象
- 页面级限制
- 内容搜索
最好的问题之一是:
当出现以下情况时,这个问题变得特别有价值:
- 父对象是公开的
- 后代有不同的可见性规则
- 搜索是通过单独的服务路径实现的
这正是这个问题出现的地方。
## 根本原因
这个 Bug 不是因为 Docmost 未能在正常的公开树中隐藏受限页面。
这个 Bug 是**公开搜索没有遵守相同的限制逻辑**。
从源码审查来看,公开树流程使用了感知限制的后代遍历。
相关区域:
- `apps/server/src/core/share/share.service.ts`
该路径通过使用以下方法有意排除了受限后代:
- `getPageAndDescendantsExcludingRestricted(...)`
但是公开分享搜索流程遵循了不同的路径。
相关区域:
- `apps/server/src/core/search/search.controller.ts`
- `apps/server/src/core/search/search.service.ts`
在那里,代码使用以下方法收集后代:
- `getPageAndDescendants(...)`
这意味着受限的后代仍然在搜索范围内。
在公开分享的上下文中,这非常重要,因为搜索分支在没有正常经过身份验证的用户权限上下文的情况下运行。因此,一旦受限的后代被包含在可搜索的页面集合中,它们的元数据就可能通过响应泄露。
### 为什么这是可利用的
因为攻击者不需要经过身份验证的账户。
他们只需要:
- 一个有效的公开分享 key
- 分享中包含的子页面
- 了解或猜测隐藏后代中可能出现的搜索词
一旦存在这个条件,公开访客就可以查询分享搜索端点并恢复:
- 隐藏的页面标题
- 高亮的正文片段
- 证明受限子页面存在于共享父页面下的证据
这足以造成机密性泄露,即使没有返回完整的页面正文。
## 是什么让这成为一个安全问题,而不仅仅是不同的端点行为
重要的区别在于,应用程序已经清楚地表明了预期的安全模型。
公开树端点隐藏了受限的后代。
所以真正的问题不是:
真正的问题是:
在 Docmost 中,它是。
这将这种情况从:
- 不一致的功能
变成了:
- 不一致的访问控制执行
这就是为什么这是一个真正的漏洞。
## PoC
我通过并排比较两个相关的公开端点验证了这个问题。
### 案例 1:公开树正确隐藏了受限子页面
首先,我使用公开分享 key 测试了正常的公开树端点。
示例请求:
```
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"
}
]
}
```
这确立了预期的产品行为:
- 受限的子页面被有意向公开访客隐藏
### 案例 2:公开分享搜索仍然泄露了受限子页面
然后,我使用一个出现在受限后代内部的词汇查询了公开分享搜索端点。
示例请求:
```
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"
}
]
}
```
这证明了核心主张:
- 受限的子页面在公开树中被隐藏了
- 但仍然通过公开分享搜索泄露了
## 为什么这两个复现很重要
这个问题最有力的部分不在于第二个请求本身。
而在于两个端点之间的对比。
### 第一点
它表明产品已经有了针对公开分享的预期限制模型。
受限的后代不应该对公开访客可见。
### 第二点
它证明了搜索路径打破了完全相同的边界。
这使得该问题更难被辩解为预期的搜索行为或文档缺失。
应用程序本身通过树响应建立了规则,然后通过搜索响应违反了它。
这是有力的证据。
## 泄露实际上给攻击者带来了什么
这个问题不会暴露整个工作空间中的任意内容。
它的范围比这要窄。
但在受影响的公开分享子树中,它仍然给攻击者提供了有用的未经授权的知识:
- 隐藏的文档标题
- 来自隐藏内容的高亮片段
- 确认受限后代的存在
- 取决于文档内容,关于薪酬、法律、计划、凭据或内部运营的线索
即使是简短的片段也很重要。
像这样的标题:
- 薪酬
- 招聘计划
- 法律草案
- 客户事件
- 凭据轮换
已经为攻击者创造了安全价值。
因此,虽然这最终被归类为**中等**,但它仍然是一个有效的机密性问题,具有清晰且合理的边界破坏。
## 严重性和分类
此问题被分配了:
- **CVE-2026-33146**
公告的严重性为:
- **中等**
- **CVSS:** `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**
最终的公告严重性为**中等**,这比更广泛的严重性声明更适合较窄的泄露范围。
这并没有削弱该发现的有效性。
它只是更精确地定义了其影响。
## 这个 Bug 实际教会了我们什么
这里的关键教训很简单:
许多开发人员只在明显的渲染路径中考虑授权:
- 页面树
- 页面视图
- 主 UI
但真正的边界更广。
你还必须问:
- 搜索是否遵守相同的规则?
- 侧信道是否遵守相同的规则?
- 元数据响应是否遵守相同的规则?
在 Docmost 中,答案是否定的。
这才是真正的要点。
## 关键点
- 公开分享功能必须在浏览和搜索路径中执行相同的可见性模型
- 当元数据泄露跨越了预期的授权边界时,它们仍然很重要
- 并排的端点比较使这类问题更有说服力
- 如果受限后代对同一个公开浏览者隐藏,它们就不应该保持可搜索状态
- 紧凑的范围不会使一个有效的 Bug 不值得报告
- 良好的安全审查通常是关于测试一致性,而不仅仅是寻找崩溃或完全绕过
## 结语
这是关于提出一个非常实际的信任边界问题。
Docmost 在一个地方隐藏了受限页面。
然后在另一个地方泄露了它。
这就是为什么它成为了 **CVE-2026-33146**。
## 攻击链
`启用了子页面的公开父分享 → 受限后代在公开树中被省略 → 攻击者查询公开分享搜索 → 受限子页面的标题和片段泄露`
## Docmost 的功能
**Docmost** 是一个协作 wiki 和文档平台。
它提供:
- 共享页面
- 公开分享链接
- 嵌套的页面树
- 工作空间和空间级的内容组织
- 跨共享内容的搜索
这意味着其公开分享模型是一个真正的安全边界。
这里的重要问题不是 Docmost 能否公开分享页面。
真正的问题是:
**当 Docmost 决定一个后代页面是受限的,并且不应该向公开分享访客展示时,这种限制是否在公开分享流程的各个环节都成立?**
在这种情况下,它并没有。
## 为什么这个 Bug 值得关注
许多安全审查在看到页面在 UI 中被隐藏后就过早地停止了。
这还不够。
更有力的问题是:
**是否每一个后端路径都执行了相同的可见性决策?**
这很重要,因为安全边界不是由界面的外观定义的。
它们是由服务器实际返回的内容定义的。
在这里,公开树端点的行为是安全的:
- 受限的后代被隐藏了
但是公开分享搜索路径的行为却不同:
- 受限的后代仍然影响了搜索结果
- 它们的标题泄露了
- 它们高亮的内容片段泄露了
这使得这是一个真正的授权和信息泄露问题,而不仅仅是一个展示上的不一致。
## 我关注的边界
我并没有通过随机模糊测试路由并期望出现有趣的结果来接触 Docmost。
更强有力的方法是首先选择一个信任边界。
- 公开分享
- 嵌套对象
- 页面级限制
- 内容搜索
最好的问题之一是:
当出现以下情况时,这个问题变得特别有价值:
- 父对象是公开的
- 后代有不同的可见性规则
- 搜索是通过单独的服务路径实现的
这正是这个问题出现的地方。
## 根本原因
这个 Bug 不是因为 Docmost 未能在正常的公开树中隐藏受限页面。
这个 Bug 是**公开搜索没有遵守相同的限制逻辑**。
从源码审查来看,公开树流程使用了感知限制的后代遍历。
相关区域:
- `apps/server/src/core/share/share.service.ts`
该路径通过使用以下方法有意排除了受限后代:
- `getPageAndDescendantsExcludingRestricted(...)`
但是公开分享搜索流程遵循了不同的路径。
相关区域:
- `apps/server/src/core/search/search.controller.ts`
- `apps/server/src/core/search/search.service.ts`
在那里,代码使用以下方法收集后代:
- `getPageAndDescendants(...)`
这意味着受限的后代仍然在搜索范围内。
在公开分享的上下文中,这非常重要,因为搜索分支在没有正常经过身份验证的用户权限上下文的情况下运行。因此,一旦受限的后代被包含在可搜索的页面集合中,它们的元数据就可能通过响应泄露。
### 为什么这是可利用的
因为攻击者不需要经过身份验证的账户。
他们只需要:
- 一个有效的公开分享 key
- 分享中包含的子页面
- 了解或猜测隐藏后代中可能出现的搜索词
一旦存在这个条件,公开访客就可以查询分享搜索端点并恢复:
- 隐藏的页面标题
- 高亮的正文片段
- 证明受限子页面存在于共享父页面下的证据
这足以造成机密性泄露,即使没有返回完整的页面正文。
## 是什么让这成为一个安全问题,而不仅仅是不同的端点行为
重要的区别在于,应用程序已经清楚地表明了预期的安全模型。
公开树端点隐藏了受限的后代。
所以真正的问题不是:
真正的问题是:
在 Docmost 中,它是。
这将这种情况从:
- 不一致的功能
变成了:
- 不一致的访问控制执行
这就是为什么这是一个真正的漏洞。
## PoC
我通过并排比较两个相关的公开端点验证了这个问题。
### 案例 1:公开树正确隐藏了受限子页面
首先,我使用公开分享 key 测试了正常的公开树端点。
示例请求:
```
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"
}
]
}
```
这确立了预期的产品行为:
- 受限的子页面被有意向公开访客隐藏
### 案例 2:公开分享搜索仍然泄露了受限子页面
然后,我使用一个出现在受限后代内部的词汇查询了公开分享搜索端点。
示例请求:
```
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"
}
]
}
```
这证明了核心主张:
- 受限的子页面在公开树中被隐藏了
- 但仍然通过公开分享搜索泄露了
## 为什么这两个复现很重要
这个问题最有力的部分不在于第二个请求本身。
而在于两个端点之间的对比。
### 第一点
它表明产品已经有了针对公开分享的预期限制模型。
受限的后代不应该对公开访客可见。
### 第二点
它证明了搜索路径打破了完全相同的边界。
这使得该问题更难被辩解为预期的搜索行为或文档缺失。
应用程序本身通过树响应建立了规则,然后通过搜索响应违反了它。
这是有力的证据。
## 泄露实际上给攻击者带来了什么
这个问题不会暴露整个工作空间中的任意内容。
它的范围比这要窄。
但在受影响的公开分享子树中,它仍然给攻击者提供了有用的未经授权的知识:
- 隐藏的文档标题
- 来自隐藏内容的高亮片段
- 确认受限后代的存在
- 取决于文档内容,关于薪酬、法律、计划、凭据或内部运营的线索
即使是简短的片段也很重要。
像这样的标题:
- 薪酬
- 招聘计划
- 法律草案
- 客户事件
- 凭据轮换
已经为攻击者创造了安全价值。
因此,虽然这最终被归类为**中等**,但它仍然是一个有效的机密性问题,具有清晰且合理的边界破坏。
## 严重性和分类
此问题被分配了:
- **CVE-2026-33146**
公告的严重性为:
- **中等**
- **CVSS:** `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**
最终的公告严重性为**中等**,这比更广泛的严重性声明更适合较窄的泄露范围。
这并没有削弱该发现的有效性。
它只是更精确地定义了其影响。
## 这个 Bug 实际教会了我们什么
这里的关键教训很简单:
许多开发人员只在明显的渲染路径中考虑授权:
- 页面树
- 页面视图
- 主 UI
但真正的边界更广。
你还必须问:
- 搜索是否遵守相同的规则?
- 侧信道是否遵守相同的规则?
- 元数据响应是否遵守相同的规则?
在 Docmost 中,答案是否定的。
这才是真正的要点。
## 关键点
- 公开分享功能必须在浏览和搜索路径中执行相同的可见性模型
- 当元数据泄露跨越了预期的授权边界时,它们仍然很重要
- 并排的端点比较使这类问题更有说服力
- 如果受限后代对同一个公开浏览者隐藏,它们就不应该保持可搜索状态
- 紧凑的范围不会使一个有效的 Bug 不值得报告
- 良好的安全审查通常是关于测试一致性,而不仅仅是寻找崩溃或完全绕过
## 结语
这是关于提出一个非常实际的信任边界问题。
Docmost 在一个地方隐藏了受限页面。
然后在另一个地方泄露了它。
这就是为什么它成为了 **CVE-2026-33146**。标签:MITM代理