0xmrma/CVE-2026-45806

GitHub: 0xmrma/CVE-2026-45806

针对 Penpot 远程图片导入功能的认证后端 SSRF 漏洞(CVE-2026-45806)的根因分析与概念验证仓库。

Stars: 0 | Forks: 0

# CVE-2026-45806 Penpot 的远程图片导入功能允许经过身份验证的文件编辑者将一项普通的媒体便捷功能转化为源自后端的 SSRF,因为攻击者控制的 URL 进入了遵循重定向的服务器获取路径,且没有进行目标过滤。 ## 简介 我在审查开源设计与代码协作平台 **Penpot** 时发现了这个问题,当时脑海中有一个非常明确的问题: **当一个协作设计工具允许用户向后端传递一个远程图片 URL 进行获取时,会发生什么?** 在这个案例中,这个问题引出了一个真实的漏洞。 Penpot 的远程图片导入流程接受用户控制的 URL,并导致后端从服务器网络上下文中获取该 URL,而没有对回环或私有网络目标强制执行目标限制。共享的 HTTP 客户端也会自动跟随重定向。 这将一项普通的媒体便捷功能转化为经过身份验证的、源自后端的 SSRF 原语,并最终成为 **CVE-2026-45806**。 **Penpot:** [GitHub 上的 Penpot](https://github.com/penpot/penpot) **CVE:** CVE-2026-45806 **CVSS:** `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N` 这影响了 **Penpot**。在其官方网站和媒体资料包中,Penpot 自称拥有 **超过 100 万且不断增长的用户群**,并表示有 **数以万计的组织** 在使用它,包括 **Blender**、**Mozilla**、**Fedora**、**NTT Data**、**MIT**、**Société Générale**、**Cisco**、**Fujitsu**、**Indra** 和 **ByteDance**。 photo0 ## 攻击链 `经过身份验证的文件编辑者 -> 攻击者控制的远程图片 URL -> create-file-media-object-from-url -> 启用重定向的后端 download-image 获取 -> 最终请求到达仅限内部的图片端点 -> 源自后端的 SSRF / 内部可达性` ## Penpot 的功能 **Penpot** 是一个开源的设计与代码协作平台。 它处理以下方面的事务: - 协作文件编辑 - 团队与项目工作流 - 上传的媒体和资产 - 渲染和预览路径 - 由服务器端处理支持的、基于浏览器的设计操作 这意味着其媒体导入路径处于一个真实的信任边界上。 这里的重要问题不是 Penpot 是否支持导入远程图片。 真正的问题是: **当用户导入远程图片时,Penpot 是否限制了后端允许连接的位置?** 在这个案例中,它没有。 ## 为什么这个漏洞值得研究 很多人低估了远程导入功能。 这是一个错误。 当应用程序出现以下情况时: - 接受攻击者控制的 URL, - 从后端发起请求, - 并将该请求转化为正常的产品工作流, 它就制造了一个真实的出站信任边界。 这就是这里的问题所在。 这个漏洞不在于图像渲染。 不在于文件存储。 也不在于编辑文件的普通权限检查。 这是一个典型的**服务器端信任失败**: - 攻击者控制的 URL 进入系统, - 后端直接获取该 URL, - 允许重定向, - 并且在审查的路径中看不到任何目标控制措施。 这足以构成一个真实的漏洞。 ## 我关注的边界 我没有盲目地模糊测试随机的 RPC 方法或首先寻找崩溃来接触 Penpot。 更有力的方法是识别最具安全潜力的边界。 对于 Penpot,那就是**远程媒体导入**。 为什么? 因为该功能结合了: - 攻击者控制的 URL 输入 - 源自后端的出站请求 - 仅在请求发出后才进行的内容验证 - 一个将成功获取视为正常媒体操作的设计工作流 这就是应该检查的正确边界。 而这正是漏洞所在之处。 ## 根本原因 该漏洞可以简化为一个小型的信任链。 在前端: ``` (defn upload-media-url [name file-id url] (rp/cmd! :create-file-media-object-from-url {:name name :file-id file-id :url url :is-local true})) ``` 用户控制的 `url` 被直接发送到 RPC 调用中。 然后在后端: ``` (sv/defmethod ::create-file-media-object-from-url ... [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (files/check-edition-permissions! pool profile-id file-id) ... (let [_ (files/get-minimal-file cfg file-id) mobj (create-file-media-object-from-url cfg (assoc params :profile-id profile-id))]) ``` 以及: ``` (defn- create-file-media-object-from-url [cfg {:keys [url name] :as params}] (let [content (media/download-image cfg url) ``` 后端检查调用者是否可以编辑目标文件,然后将攻击者控制的 URL 传递给 `media/download-image`。 获取功能的实现如下: ``` (defn download-image "Download an image from the provided URI and return the media input object" [{:keys [::http/client]} uri] ... (http/req! client {:method :get :uri uri} {:response-type :input-stream}) ``` 共享的 HTTP 客户端配置为: ``` (http/build-client {:connect-timeout 30000 :follow-redirects :always})) ``` 这就是漏洞的全部所在: - 攻击者控制 URL - 后端执行请求 - 自动跟随重定向 - 在发出请求前未应用任何目标过滤 ### 为什么这是可利用的 因为攻击者只需要: - 一个有效的 Penpot 账户 - 对一个文件的编辑权限 - 一个返回被接受的图像内容的目标 攻击链非常直接: - 攻击者提供一个 URL - Penpot 从后端获取它 - 第一跳可以是公开的或看起来无害的 - 重定向目标可以是内部的 - 如果最终响应看起来像是一个允许的图像,导入就会完成 这就是漏洞的全貌。 ## 为什么这是一个安全问题,而不仅仅是正常的远程导入行为 重要的区别在于**请求发生的地点**。 问题不在于: 真正的问题在于: 在这个案例中,答案是肯定的。 这很重要,因为以下两者之间存在实质性的区别: - 浏览器获取用户提供的 URL,与 - 后端从服务器网络位置获取该 URL 图像验证并不能消除这种区别。 它缩小了一些直接的数据泄露案例,但它并没有消除 SSRF 条件或打破网络边界。 ## PoC 我通过一个直接关联到所审查 Penpot 代码路径的受控本地证明验证了此问题。 目标不是攻击第三方基础设施。 目标是为了证明确切的安全属性: - 后端风格的请求执行 - 跟随重定向 - 成功向仅限内部的端点进行枢纽转移 - 在 Penpot 强制执行的相同面向图像的约束下完成 我构建了一个自包含的 Java 验证器,它镜像了相关的行为: - 后端侧对调用者控制的 URI 执行 GET 请求 - 自动跟随重定向 - 基于 `content-type` 和 `content-length` 的图像接受检查 我验证了两种情况。 ### 案例 1:直接内部获取 验证器请求: ``` http://127.0.0.1:7790/internal.png ``` 观察到结果: - 请求的 URI:`http://127.0.0.1:7790/internal.png` - 最终 URI:`http://127.0.0.1:7790/internal.png` - 状态:`200` - 内容类型:`image/png` - 工件(artifact)写入成功 这证明了导入式的获取逻辑直接接受了一个仅限内部的图像端点。 ### 案例 2:重定向辅助的内部获取 验证器随后请求: ``` http://localhost:7791/redirect-to-internal ``` 该端点返回了一个 HTTP 重定向到: ``` http://127.0.0.1:7790/internal.png ``` 观察到结果: - 请求的 URI:`http://localhost:7791/redirect-to-internal` - 最终 URI:`http://127.0.0.1:7790/internal.png` - 状态:`200` - 内容类型:`image/png` - 工件(artifact)写入成功 仅限内部的监听器记录了重定向的请求。 这证明了更重要的主张: - 最初的攻击者控制的 URL 可以与最终目标不同 - 会自动跟随重定向 - 最终的后端获取可以到达仅限内部的端点并仍然成功 ## 为什么 PoC 要这样构建 这里的 payload 被故意设计得很简单: - 微小的有效 PNG 响应 - 明确的重定向目标 - 绑定到回环地址的仅限内部的监听器 这很重要,因为 Penpot 并不是仅仅获取任意字节然后就停止。 它会在请求后执行面向媒体的验证。 因此,正确的证明不是: 更有力的证明是: 这正是验证所证明的内容。 ## 为什么这仍然值得报告 对于此类 SSRF 漏洞,一种常见的反应是: 这种观察是正确的,但不全面。 它不能消除漏洞。 它只是告诉你哪些内部目标是最直接有用的。 此问题仍然可以实现: - 源自后端的内部可达性 - 重定向辅助向回环或私有网络空间的枢纽转移 - 与返回图像的内部端点进行交互 - 滥用 Penpot 服务器位置的网络信任 这仍然是一个真实的安全边界突破。 特别是在自托管环境中,内部服务通常就是专门存在于该边界之后的。 ## 严重性及分类 此问题最终被评定为 **高** 严重性 CVSS: - **CWE-918**: Server-Side Request Forgery (SSRF) - **CVSS:** ``` CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N ``` 这种分类是有道理的。 其主张并不是说未经身份验证的攻击者可以凭空立即攻破每个 Penpot 部署。 其主张是,任何普通且经过身份验证的文件编辑者都可以将 Penpot 转变为针对内部目标的后端请求原语,包括通过重定向辅助访问回环和私有网络目标。 在披露过程中,有一些关于严重性的讨论,主要围绕: - 内部服务需要身份验证 - 获取的内容需要通过图像验证 - 利用取决于对内部基础设施的了解 这些都是值得讨论的合理约束。 但它们并不能消除核心问题: - 攻击者控制的 URL - 后端侧的请求源 - 跟随重定向 - 在审查的路径中没有出站目标策略 这是一个真实且有据可查的 SSRF 漏洞。 ## 修复分析 这里重要的修复不是更严格的 MIME 处理。 真正的修复是**出站目标策略**。 针对此类漏洞的正确补救措施需要: 1. 仅允许 `http` 和 `https` 2. 在连接前解析并拒绝回环、RFC1918/私有、链路本地、多播、未指定以及元数据服务范围 3. 根据相同策略重新检查每个重定向跃点 4. 考虑为此功能禁用重定向或对其进行严格限制 5. 添加针对以下情况的回归测试覆盖: - `localhost` - 直接私有目标 - 重定向到私有的情况 - DNS 重绑定风格场景 这是正确的修复方向,因为这不是一个图像解析漏洞。 这是一个网络信任边界漏洞。 ## 披露 此问题是通过 GitHub 的安全报告流程私下报告的。 报告包括: - 源代码级别的根本原因分析 - 强有力的本地验证模型 - 基于重定向的内部枢纽转移证明 - 工件(artifact)和日志证据 - 补救指导 维护者确认了该问题并开始着手解决。 该问题随后被分配了: **CVE-2026-45806** ## 这个漏洞真正教会了我们什么 这里的关键教训很简单: 很多开发人员的思维模式是: - URL 被接受 - 请求成功 - 图像通过验证 - 媒体被存储 这些都是实现细节。 真正的安全问题是: **后端允许代表用户连接到哪里?** 如果没有明确回答这个问题,像远程导入这样的功能默认就会成为 SSRF 的攻击面。 这个漏洞还印证了关于 SSRF 审查的一些重要事项: - 重定向很重要 - 内容验证不能替代网络策略 - 当跨越内部信任边界时,经过身份验证的 SSRF 仍然是很严重的 这是真正的核心要点。 ## 关键点 - 远程图片导入是一个真实的后端信任边界 - 经过身份验证的功能仍然可能暴露出严重的 SSRF - 跟随重定向使得出站获取路径更加危险 - 仅图像验证缩小了一些滥用路径,但不能消除 SSRF - 证明成功的内部重定向路径比仅仅展示失败的连接尝试更有力 - 正确的修复是出站目标策略,而不是表面的响应验证 ## 结语 这个漏洞不在于一个花哨的 payload。 它在于提出了正确的信任边界问题。 Penpot 允许经过身份验证的文件编辑者提供远程图像 URL,而后端对该 URL 的信任超出了应有的程度。 重定向处理完成了其余的工作。 这就是它成为 **CVE-2026-45806** 的原因。
标签:JS文件枚举, SSRF, Web安全, 漏洞分析, 蓝队分析, 设计协作, 路径探测