Penpot 的远程图片导入功能使得一个经过身份验证的文件编辑器能够将普通的媒体便利功能转变为后端正向 SSRF,因为攻击者控制的 URL 进入了跟随重定向的服务器请求路径,而该路径没有进行目标过滤。
我在审计 Penpot(开源设计和代码协作平台)时发现了这个问题,当时我带着一个非常具体的问题:
当协作设计工具允许一个用户将远程图片 URL 交给后端获取时,会发生什么?
在这个案例中,这个问题最终导致了一个真实的漏洞。
Penpot 的远程图片导入流程接受用户控制的 URL,并让后端在服务器网络上下文中获取该 URL,而没有对回环或私有网络目标施加目标限制。共享的 HTTP 客户端还自动跟随重定向。
这就把一个普通的媒体便利功能变成了一种经过身份验证的后端正向 SSRF 原语,并最终成为了 CVE-2026-45806。
Penpot: Penpot on GitHub
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。
身份验证的文件编辑器 -> 攻击者控制的远程图片 URL -> create-file-media-object-from-url -> 后端 download-image 请求(启用重定向) -> 最终请求到达仅内部可访问的图片端点 -> 后端发起 SSRF / 内部可达性
Penpot 是一个开源的设计和代码协作平台。
它处理诸如:
这意味着其媒体导入路径处于真实的信任边界上。
这里重要的问题不是 Penpot 是否支持导入远程图片。
真正的问题是:
当用户导入远程图片时,Penpot 是否限制了后端允许连接的目的地?
在这个案例中,它没有。
很多人低估了远程导入功能。
这是一个错误。
一旦应用程序:
它就会创建一个真实的出站信任边界。
这就是问题所在。
这个漏洞不在图像渲染中。 也不在文件存储中。 也不在普通的文件编辑权限检查中。
这是一个典型的 服务器端信任失败:
这足以造成一个真实的漏洞。
我并不是盲目地对 Penpot 的随机 RPC 方法进行模糊测试或寻找崩溃来入手。
更强的方法是识别出最有前途的安全边界。
对于 Penpot 来说,那就是 远程媒体导入。
为什么?
因为这个功能结合了:
那是值得审视的安全边界。
而漏洞正好就在那里。
这个漏洞归结为一条短小的信任链。
在前端:
(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
"从提供的 URI 下载图像并返回媒体输入对象"
[{: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}))
这就是整个漏洞:
因为攻击者只需要:
攻击链非常直接:
这就是整个漏洞。
重要的区别在于请求发生的位置。
问题不是:
"Penpot 可以从 URL 导入图片吗?"
真正的问题是:
"经过身份验证的用户能否让 Penpot 后端连接到用户本不应通过应用程序到达的内部目的地?"
在这个案例中,答案是肯定的。
这很重要,因为:
之间存在真正的区别。
图像验证并不能消除这种区别。
它缩小了一些直接数据外泄的情况,但并不能消除 SSRF 条件或网络边界突破。
我通过一个受控的本地证明验证了这个问题,该证明直接关联到审计的 Penpot 代码路径。
目标不是攻击第三方基础设施。 目标是证明确切的安全属性:
我构建了一个自包含的 Java 验证器,模拟了相关行为:
content-type 和 content-length 的图片接受检查。我验证了两个案例。
验证器请求:
http://127.0.0.1:7790/internal.png
观察结果:
http://127.0.0.1:7790/internal.pnghttp://127.0.0.1:7790/internal.png200image/png这证明了导入风格的请求逻辑直接接受了一个内部唯一的图片端点。
验证器然后请求:
http://localhost:7791/redirect-to-internal
该端点返回 HTTP 重定向到:
http://127.0.0.1:7790/internal.png
观察结果:
http://localhost:7791/redirect-to-internalhttp://127.0.0.1:7790/internal.png200image/png内部唯一监听器记录了被重定向的请求。
这证明了更重要的主张:
这里的负载故意简单:
这很重要,因为 Penpot 不仅仅获取任意字节就停止。 它在请求之后执行面向媒体的验证。
所以正确的证明不是:
"后端可以尝试连接到某个地方"
更强的证明是:
"后端可以被强制连接到某个内部地址,并在该功能期望的相同图片约束下成功完成请求"
这正是验证所展示的。
对于像这样的 SSRF 漏洞,常见的反应是:
"目标仍然必须返回一张图片"
这个观察是正确的,但不够全面。
它并不能消除漏洞。
它只是告诉你哪些内部目标最直接有用。
这个问题仍然允许:
这仍然是一个真实的安全边界突破。
尤其是在自托管环境中,内部服务通常正是存在于该边界之后。
该问题最终被分配了高严重性的 CVSS:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
这个分类是合理的。
这并不是说未经身份验证的攻击者可以从零开始立即攻陷每一个 Penpot 部署。
而是说明任何一个普通的经过身份验证的文件编辑器都可以将 Penpot 转变为针对内部目的地的后端请求原语,包括通过重定向辅助访问回环和私有网络目标。
在披露过程中,关于严重性有一些讨论,主要集中在:
这些是可以讨论的合理限制。
但它们并不能消除核心问题:
这是一个真实且站得住脚的 SSRF 漏洞。
重要的修复点不是更严格的 MIME 处理。
真正的修复是出站目的地策略。
对此类漏洞的正确修复需要:
http 和 httpslocalhost这是正确的修复方向,因为这不是一个图像解析漏洞。 这是一个网络信任边界漏洞。
该问题通过 GitHub 的安全报告流程私下报告。
报告包含:
维护人员确认了该问题并开始研究解决方案。
该问题后来被分配了:
CVE-2026-45806
关键的教训很简单:
远程媒体导入是一个出站信任边界,而不仅仅是一个便利功能
许多开发人员倾向于思考:
这些都是实现细节。
真正的安全问题是:
后端被允许代表用户连接到何处?
如果没有明确回答这个问题,远程导入等功能默认就会成为 SSRF 攻击面。
这个漏洞还强化了关于 SSRF 审计的重要一点:
这才是真正的要点。
这个漏洞与花哨的载荷无关。
它关乎提出正确的信任边界问题。
Penpot 允许经过身份验证的文件编辑器提供一个远程图片 URL,而后端对该 URL 的信任超出了应有的范围。 重定向处理则完成了其余的工作。
这就是为什么它成为了 CVE-2026-45806。