Skip to content
KitploitKITPLOIT
工具博客
Log in
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

订阅源联系隐私© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2026-34213 — 一个低权限的Docmost用户可以向通用上传端点提供一个受害者的attachmentId,并覆盖同一工作区内另一个页面存储的附件。 | Kitploit
工具/GitHubGitHub/0xmrma/cve-2026-34213
漏洞分析Web应用程序漏洞利用渗透测试论文与研究学习与教育
GitHub0xmrma/cve-2026-34213

CVE-2026-34213

一个低权限的Docmost用户可以向通用上传端点提供一个受害者的attachmentId,并覆盖同一工作区内另一个页面存储的附件。

查看仓库
533个月前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2026-34213

一个低权限的 Docmost 用户可以向通用上传端点提供一个受害者的 attachmentId,并覆盖同一工作空间内另一页面的存储附件。

引言

我在 Docmost(开源协作文档平台)中发现、负责任地披露并复现了一个 高严重性 的授权缺陷。

Docmost 官网将其描述为可本地部署、适合企业的 wiki,拥有 300万+ 次下载,并表示受到包括 维尔纽斯市、Bechtle、澳大利亚政府、红十字会 和 ETS Quebec 在内的团队信任。

该漏洞存在于 Docmost 也用于图表保存/更新流程的通用文件上传路径中。

我当时带着一个非常具体的问题审查了那段代码:

如果上传端点验证了对某个页面的编辑权限,但覆盖目标是由用户单独控制的 attachmentId 选择的,会发生什么?

在这种情况下,这个问题直接导致了一个真实的对象绑定失败。

Docmost 允许调用者发送:

  • 一个他们有权编辑的页面的 pageId,以及
  • 一个属于同一工作空间内不同页面的 attachmentId

服务器确实执行了覆盖一致性检查,但守卫使用了错误的布尔逻辑。

这意味着请求可能通过授权并无论如何覆盖受害者附件。

此问题被分配为 CVE-2026-34213。

Docmost: docmost/docmost
安全公告: GHSA-89fp-2hch-j9gp
CVE: CVE-2026-34213
已修复版本: v0.71.0

photo0 ---

攻击链

攻击者控制的具有编辑权限的 pageId -> 攻击者控制的受害者 attachmentId -> 有缺陷的覆盖守卫将跨页面覆盖视为有效 -> 从受害者 attachmentId 重建存储路径 -> 攻击者字节替换受害者文件 -> 受害者页面继续提供修改后的附件


Docmost 此部分功能简介

Docmost 将上传的页面附件存储为数据库记录以及存储中的备份文件。

对于正常上传,服务器会创建新的附件 ID 并写入新文件。

然而,对于图表保存/更新流程,客户端有意重用现有的 attachmentId,以便同一图表文件可以在原地更新,而不是每次生成全新的附件记录。

这种行为本身是合理的。

问题在于它创建了一个高风险路径:

  • 一个输入用于标识被授权的页面
  • 另一个输入用于标识被覆盖的附件

每当一个端点混合了这两个职责时,实现必须将它们精确地绑定在一起。

Docmost 没有做到这一点。


为什么这个攻击面值得关注

混合创建/更新端点是授权漏洞的常见来源。

原因很简单:

  • 创建流程通常针对容器对象进行授权
  • 更新流程通常针对现有记录进行授权
  • 如果一个端点试图同时做这两件事,很容易先验证错误的东西,而将第二个标识符视为“只是元数据”

这正是这里的模式。

POST /api/files/upload 验证了调用者可以编辑由 pageId 命名的页面。

但如果也提供了 attachmentId,服务器会切换到覆盖路径并单独选择一个现有的附件记录。

这就产生了关键的安全问题:

覆盖路径是否验证了所选附件确实属于被授权的页面?

易受攻击版本中的答案是否定的。


根本原因

根本原因是 通过用户控制的密钥绕过授权,再加上覆盖守卫中的布尔逻辑错误。

易受攻击的流程如下:

  1. AttachmentController.uploadFile() 从多部分表单数据中读取 pageId。
  2. 它加载该页面并调用 validateCanEdit(page, user)。
  3. 它另外接受来自同一请求的可选 attachmentId。
  4. AttachmentService.uploadFile() 通过攻击者提供的 ID 加载现有附件。
  5. 覆盖守卫尝试验证现有附件是否与被授权的页面匹配。
  6. 守卫使用了 && 而不是在有任一不匹配时拒绝。

易受攻击的守卫是:

if (
  existingAttachment.pageId !== pageId &&
  existingAttachment.fileExt !== preparedFile.fileExtension &&
  existingAttachment.workspaceId !== workspaceId
) {
  throw new BadRequestException("File attachment does not match");
}

该条件仅在以下情况同时成立时才拒绝请求:

  • 页面 ID 不匹配,且
  • 文件扩展名不匹配,且
  • 工作空间 ID 不匹配

这与覆盖守卫应该做的正好相反。

对于真实的攻击场景,攻击者有意停留在同一工作空间内。

所以:

  • existingAttachment.workspaceId !== workspaceId 为 false

一旦该操作数为 false,整个 && 条件评估为 false,即使附件属于不同的页面。

因此服务器将跨页面覆盖视为有效。

这是漏洞的前半部分。

后半部分是影响变得真实的原因。

检查之后,服务使用攻击者提供的 attachmentId 和文件名重建了目标存储路径:

const filePath =
  `${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/` +
  `${attachmentId}/${preparedFile.fileName}`;

然后,在更新路径上,Docmost 只更新了可变元数据,例如:

  • fileSize
  • updatedAt

它没有将所有权重新绑定到攻击者的页面。

因此受害者页面继续指向相同的附件记录和相同的附件 ID。 只有底层文件字节发生了变化。

这就是为什么这不是一个无害的不匹配问题。

这是一个持久的、未经授权的覆盖原语。


为什么这是安全问题,而不仅仅是逻辑错误

这不是一个表面上的 bug,也不是文件名冲突问题。

攻击者不需要竞争条件。 攻击者不需要猜测随机路径。 攻击者不需要对受害者页面的写入权限。

他们只需要:

  • 读取权限以了解受害者附件引用,以及
  • 对同一工作空间内任何其他页面的写入权限

从那里,他们可以替换另一个页面附件的存储文件字节,而受害者页面继续引用并提供该附件,就好像什么都没变一样。

这是一个直接的完整性失败。

在实际情况下,攻击者可以:

  • 篡改图表
  • 用误导性内容替换附件
  • 损坏引用的文件
  • 创建混乱的审计痕迹,因为附件看起来仍属于受害者页面

关键点是:

服务器接受了攻击者选择的覆盖目标,而没有将其绑定到实际检查了编辑权限的页面上。

这是一个访问控制失败,而不仅仅是糟糕的布尔卫生。


为什么利用是可行的

对于图表附件来说,利用尤其可行。

Docmost 的客户端在图表保存时有意重用 attachmentId,并使用确定性的文件名:

  • diagram.excalidraw.svg
  • diagram.drawio.svg

这很重要,因为它降低了攻击者的需求。

对于通用附件,攻击者需要同时具备:

  • 受害者附件 ID
  • 受害者文件名

对于图表,文件名已经是可预测的。

所以如果攻击者可以读取受害者页面内容,他们通常可以恢复他们唯一缺失的部分:

  • 受害者 attachmentId

在我的验证设置中,我正好使用了这条路径:

  • 攻击者仅对受害者空间具有读取者访问权限
  • 攻击者对另一个攻击者控制的空间具有写入者访问权限
  • 两个空间属于同一个工作空间

这已经足够。

利用跨越了同一工作空间内的页面边界和空间边界,同时仍然满足了有缺陷的工作空间检查。


概念验证

我针对 Docmost v0.70.3 使用基于 docmost/docmost:0.70.3、Postgres 和 Redis 构建的一次性实验室进行了现场验证。

PoC 流程:

  1. 创建一个所有者账户。
  2. 在同一工作空间中创建一个受害者空间和一个攻击者控制的空间。
  3. 邀请第二个用户作为攻击者。
  4. 授予攻击者:
    • 对受害者空间的读取者访问权限
    • 对攻击者控制的空间的写入者访问权限
  5. 在受害者空间中,向受害者页面上传一个图表附件。
  6. 作为攻击者,检索受害者页面信息并记下受害者 attachmentId。
  7. 发送 POST /api/files/upload,包含:
    • pageId = 攻击者页面 ID
    • attachmentId = 受害者附件 ID
    • file = 使用受害者文件名的攻击者控制替换文件
  8. 在覆盖前后下载受害者附件并比较哈希值。

最小的请求格式为:

POST /api/files/upload
Content-Type: multipart/form-data

pageId=<attackerPageId>
attachmentId=<victimAttachmentId>
[email protected];filename=diagram.excalidraw.svg

观察到的现场结果:

  • 攻击者得知的受害者附件 ID:019d18ae-b176-751c-8525-b5f3cede131d
  • 用于覆盖请求的攻击者页面 ID:019d18ae-b15b-70e9-ac67-64948e87cc5e
  • 受害者所有者页面 ID 保持为:019d18ae-b12f-75ec-8c1c-5aff3ba6be9c
  • 服务器对覆盖请求的响应:200 OK
  • 覆盖前受害者文件的 SHA-256:
686a0a0ede90ece1cbb975bb29304a6c3a90373a9c3ab2496345cf7ca59cc8fa
  • 覆盖后受害者文件的 SHA-256:
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
  • 攻击者负载的 SHA-256:
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
  • 挂载的存储确认受害者路径现在包含:
来自另一页面的攻击者替换内容

这是一个完整的端到端覆盖证明,而不仅仅是理论上的源代码审查。


为什么以这种方式选择 PoC

在分类期间,我使用了两种证明风格:

  • 一个镜像易受攻击覆盖逻辑的窄独立测试工具,以及
  • 针对一次性 Docmost 实例的完整实时 HTTP 漏洞利用

独立测试工具有助于隔离布尔逻辑错误。

实时 HTTP PoC 是更有力的证据,因为它证明了完整的安全故事:

  • 页面授权在攻击者页面上成功
  • 受害者 attachmentId 被接受
  • 覆盖请求返回成功
  • 受害者页面保持逻辑所有者地位
  • 磁盘上存储的字节变为攻击者控制的内容

这种区别在访问控制漏洞中很重要。

“条件错误”本身是不够的。

“条件错误,并且应用程序可以被端到端驱动到持久的未经授权覆盖”才是完整的案例。


修复分析

修复在 v0.71.0 中发布,并将覆盖守卫从 && 改为 ||:

if (
  existingAttachment.pageId !== pageId ||
  existingAttachment.fileExt !== preparedFile.fileExtension ||
  existingAttachment.workspaceId !== workspaceId
) {
  throw new BadRequestException("File attachment does not match");
}

这个补丁对于报告的漏洞来说是最小、直接且正确的。

它恢复了正确的规则:

仅当现有附件与被授权的页面/工作空间/类型假设完全匹配时,才允许覆盖。

一旦守卫在任何不匹配时拒绝:

  • 跨页面覆盖失败
  • 跨工作空间覆盖失败
  • 类型/扩展名不匹配失败

这是正确的修复方式:

  • 无需重新设计
  • 无需模糊的兼容性逻辑
  • 无需尝试“尽力而为”恢复

只是在被授权的页面和覆盖目标之间进行了严格的绑定。

这里还有一个更广泛的设计教训:

同时服务于原地更新流程的通用上传端点应被视为高风险 API 表面。

即使当前的 bug 被修复了,更强健的长期设计是:

  • 用于图表保存流程的专用更新端点
  • 对文件名以及附件 ID 进行不可变的绑定检查
  • 明确模拟跨页面覆盖尝试的回归测试

但对于漏洞本身,已发布的补丁干净地关闭了核心问题。


重要的回归测试用例

无论项目是否为自己添加了关于此修复的私有测试,以下用例对长期覆盖都很重要:

  • 使用相同页面 ID 和相同附件 ID 进行覆盖应成功
  • 使用不同页面 ID 但相同工作空间进行覆盖应失败
  • 使用不同工作空间进行覆盖应失败
  • 文件扩展名不匹配的覆盖应失败
  • 使用已知图表文件名但外部附件 ID 的覆盖应失败
  • 覆盖应永不静默地在写入攻击者控制的字节后保留受害者所有权

这些测试的目的不仅仅是正确性。

而是锁定授权绑定,以便未来的“友好”上传重构不会重新打开同一类漏洞。


严重性与分类

已发布的安全公告将此问题分类为:

  • CWE-639:通过用户控制的密钥绕过授权
  • CVSS v3.1:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L

最终得分为 7.1 / 高,这是正确的结论。

这里的重要指标是完整性。

这不是一个低级别的元数据 bug。 攻击者完全控制了写入另一个页面附件路径的替换字节,并且受害者页面在之后继续提供修改后的对象。

这正是那种值得 完整性高 的跨记录篡改。

可用性保持低也是合理的,因为损坏图表或附加文档可能使受害者内容无法使用,但主要影响仍然是未经授权的修改,而不是完全的服务中断。


披露

我通过 GitHub 安全公告私下报告了该问题,内容包括:

  • 根本原因分析
  • 实时 HTTP PoC
  • 请求/响应证据
  • 覆盖前后文件哈希值
  • 固定的一次性实验室设置

该问题被维护者接受,分配了 CVE-2026-34213,并于 2026年4月14日 发布。

公开公告中列出了:

  • 受影响版本:>= v0.3.0
  • 修复版本:v0.71.0

该历史记录也与我本地的源代码审查一致: 在我检查的易受攻击行中,最早的可标记版本中就已存在易受攻击的覆盖逻辑。


这个漏洞真正教会了我们什么

这里有趣的教训不仅仅是“使用 || 而不是 &&。”

那是症状。

更深层次的经验是:

如果一个用户控制的字段证明授权,而另一个用户控制的字段选择要更新的对象,那么这两个字段必须显式且精确地绑定在一起。

这个规则随处可见:

  • 文档附件
  • 个人资料媒体
  • 云对象引用
  • 问题/评论编辑
  • 后台作业重新处理

当系统宣称:

  • “你可以编辑页面 X”
  • “同时请告诉我哪个现有记录要更新”

它就创造了一个必须用精确匹配不变量来强制执行的安全边界。

任何比这更宽松的最终都会变成用户控制的密钥漏洞。

这个问题还强化了第二个容易被低估的观点:

守卫代码中的小布尔错误可能产生第一级安全后果。

一个“看起来合理”的三子句条件足以反转覆盖路径的保护模型。

这就是为什么这些攻击面需要经过深思熟虑的审查,而不是随意的信心。


关键点

  • Docmost 使用同一个端点处理新上传和原地附件更新。
  • 授权是根据调用者提供的 pageId 进行检查的,但覆盖目标选择使用了单独的调用者提供的 attachmentId。
  • 覆盖守卫仅在所有不匹配条件同时为真时才拒绝。
  • 在同一工作空间的攻击场景中,该检查开放失败。
  • 服务从受害者附件 ID 重建存储路径,并将攻击者控制的字节写入其中。
  • 附件记录在覆盖后仍然绑定在受害者页面上。
  • 确定性的图表文件名使得利用尤其可行。
  • v0.71.0 中的修复正确地将守卫改为在有任何不匹配时拒绝。

结语

这个漏洞并非关于奇特的存储行为。

而是关于一个更新路径过于信任攻击者选择的对象标识符。

Docmost 证明了在一个页面上的编辑权限,接受了来自另一个页面的现有附件 ID,然后让一个有缺陷的覆盖检查将这种不匹配变成了成功的跨页面文件替换。

这就是它成为 CVE-2026-34213 的原因。

v0.71.0 中的补丁干净地修复了直接的问题,但更广泛的教训仍然有价值:

当授权和对象选择被分割到不同的用户控制字段时,精确绑定就是安全属性。

下载工具