
Penpot's remote image import let an authenticated file editor turn a normal media convenience feature into backend-origin SSRF because attacker-controlled URLs crossed into a redirect-following server fetch path without destination filtering.
Penpot's remote image import let an authenticated file editor turn a normal media convenience feature into backend-origin SSRF because attacker-controlled URLs crossed into a redirect-following server fetch path without destination filtering.
I found this issue while reviewing Penpot, the open-source design and code collaboration platform, with a very specific question in mind:
What happens when a collaborative design tool lets one user hand the backend a remote image URL to fetch?
In this case, that question led to a real bug.
Penpot's remote image import flow accepted a user-controlled URL and caused the backend to fetch it from the server network context without enforcing destination restrictions for loopback or private-network targets. The shared HTTP client also followed redirects automatically.
That turned a normal media convenience feature into an authenticated backend-origin SSRF primitive and ultimately became 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
This affected Penpot. On its official site and media kit, Penpot presents itself as having a +1M growing user base and says that tens of thousands of organizations use it, including Blender, Mozilla, Fedora, NTT Data, MIT, Société Générale, Cisco, Fujitsu, Indra, and ByteDance.
authenticated file editor -> attacker-controlled remote image URL -> create-file-media-object-from-url -> backend download-image fetch with redirects enabled -> final request lands on internal-only image endpoint -> backend-origin SSRF / internal reachability
Penpot is an open-source design and code collaboration platform.
It handles things like:
That means its media import path sits on a real trust boundary.
The important question here was not whether Penpot supports importing remote images.
The real question was:
Does Penpot restrict where the backend is allowed to connect when a user imports a remote image?
In this case, it did not.
A lot of people underestimate remote import features.
That is a mistake.
The moment an application:
it creates a real outbound trust boundary.
That was the issue here.
This bug was not in image rendering. It was not in file storage. It was not in ordinary permission checks for editing a file.
It was a classic server-side trust failure:
That is enough to create a real vulnerability.
I did not approach Penpot by blindly fuzzing random RPC methods or looking for crashes first.
The stronger approach was to identify the most promising security boundary.
For Penpot, that was remote media import.
Why?
Because this feature combines:
That was the right boundary to inspect.
And it was exactly where the bug lived.
The bug reduces to a small trust chain.
In the frontend:
(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}))
the user-controlled url is sent directly into the RPC call.
Then in the backend:
(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))])
and:
(defn- create-file-media-object-from-url
[cfg {:keys [url name] :as params}]
(let [content (media/download-image cfg url)
the backend checks that the caller can edit the target file, then passes the attacker-controlled URL into media/download-image.
The fetch implementation is here:
(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})
And the shared HTTP client is configured as:
(http/build-client {:connect-timeout 30000
:follow-redirects :always}))
That is the whole vulnerability:
Because the attacker only needs:
The attack chain is straightforward:
That is the whole bug.
The important distinction is where the request happens.
The question is not:
"Can Penpot import images from URLs?"
The real question is:
"Can an authenticated user make the Penpot backend connect to internal destinations that the user should not be able to reach through the application?"
In this case, the answer was yes.
That matters because there is a real difference between:
Image validation does not remove that difference.
It narrows some direct exfiltration cases, but it does not remove the SSRF condition or the network boundary break.
I validated this issue with a controlled local proof tied directly to the reviewed Penpot code path.
The goal was not to hit third-party infrastructure. The goal was to prove the exact security property:
I built a self-contained Java validator that mirrored the relevant behavior:
content-type and content-lengthI validated two cases.
The validator requested:
http://127.0.0.1:7790/internal.png
Observed result:
http://127.0.0.1:7790/internal.pnghttp://127.0.0.1:7790/internal.png200image/pngThat proved the import-style fetch logic accepted an internal-only image endpoint directly.
The validator then requested:
http://localhost:7791/redirect-to-internal
That endpoint returned an HTTP redirect to: