Share
## https://sploitus.com/exploit?id=PACKETSTORM:224373
# CVE-2026-45806
    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.
    ## Intro
    
    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](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`
    
    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**.
    
    <img width="840" height="560" alt="photo0" src="https://github.com/user-attachments/assets/57e14d6a-a6f1-41ba-982e-c87ac90a2a26" />
    
    ---
    
    ## Attack Chain
    
    `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`
    
    ---
    
    ## What Penpot Does
    
    **Penpot** is an open-source design and code collaboration platform.
    
    It handles things like:
    - collaborative file editing
    - team and project workflows
    - uploaded media and assets
    - rendering and preview paths
    - browser-based design operations backed by server-side processing
    
    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.
    
    ---
    
    ## Why This Bug Was Worth Looking At
    
    A lot of people underestimate remote import features.
    
    That is a mistake.
    
    The moment an application:
    - accepts an attacker-controlled URL,
    - makes the request from the backend,
    - and turns that request into a normal product workflow,
    
    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**:
    - an attacker-controlled URL entered the system,
    - the backend fetched it directly,
    - redirects were allowed,
    - and no destination controls were visible in the reviewed path.
    
    That is enough to create a real vulnerability.
    
    ---
    
    ## The Boundary I Focused On
    
    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:
    - attacker-controlled URL input
    - backend-origin outbound requests
    - content validation that happens only after the request is made
    - a design workflow where successful fetches are treated as normal media operations
    
    That was the right boundary to inspect.
    
    And it was exactly where the bug lived.
    
    ---
    
    ## Root Cause
    
    The bug reduces to a small trust chain.
    
    In the frontend:
    
    ```clojure
    (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:
    
    ```clojure
    (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:
    
    ```clojure
    (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:
    
    ```clojure
    (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:
    
    ```clojure
    (http/build-client {:connect-timeout 30000
                        :follow-redirects :always}))
    ```
    
    That is the whole vulnerability:
    
    - attacker controls the URL
    - backend performs the request
    - redirects are followed automatically
    - no destination filtering is applied before the request is made
    
    ### Why this is exploitable
    
    Because the attacker only needs:
    - a valid Penpot account
    - edit permission on one file
    - a target that returns accepted image content
    
    The attack chain is straightforward:
    
    - attacker supplies a URL
    - Penpot fetches it from the backend
    - the first hop can be public or apparently harmless
    - the redirect target can be internal
    - if the final response looks like an allowed image, the import completes
    
    That is the whole bug.
    
    ---
    
    ## What Makes This a Security Issue, Not Just Normal Remote Import Behavior
    
    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:
    - a browser fetching a user-supplied URL, and
    - the backend fetching that URL from the server network position
    
    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.
    
    ---
    
    ## PoC
    
    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:
    
    - backend-style request execution
    - redirect following
    - successful pivot to an internal-only endpoint
    - completion under the same image-oriented constraints Penpot enforces
    
    I built a self-contained Java validator that mirrored the relevant behavior:
    - backend-side GET to a caller-controlled URI
    - automatic redirect following
    - image acceptance checks based on `content-type` and `content-length`
    
    I validated two cases.
    
    ### Case 1: direct internal fetch
    
    The validator requested:
    
    ```text
    http://127.0.0.1:7790/internal.png
    ```
    
    Observed result:
    
    - requested URI: `http://127.0.0.1:7790/internal.png`
    - final URI: `http://127.0.0.1:7790/internal.png`
    - status: `200`
    - content type: `image/png`
    - artifact written successfully
    
    That proved the import-style fetch logic accepted an internal-only image endpoint directly.
    
    ---
    
    ### Case 2: redirect-assisted internal fetch
    
    The validator then requested:
    
    ```text
    http://localhost:7791/redirect-to-internal
    ```
    
    That endpoint returned an HTTP redirect to:
    
    ```text
    http://127.0.0.1:7790/internal.png
    ```
    
    Observed result:
    
    - requested URI: `http://localhost:7791/redirect-to-internal`
    - final URI: `http://127.0.0.1:7790/internal.png`
    - status: `200`
    - content type: `image/png`
    - artifact written successfully
    
    The internal-only listener logged the redirected request.
    
    That proved the more important claim:
    - the initial attacker-controlled URL can differ from the final destination
    - redirects are followed automatically
    - the final backend fetch can land on an internal-only endpoint and still succeed
    
    ---
    
    ## Why the PoC Was Built This Way
    
    The payload here was intentionally simple:
    
    - tiny valid PNG response
    - explicit redirect target
    - internal-only listener bound to loopback
    
    That mattered because Penpot does not just fetch arbitrary bytes and stop.
    It performs media-oriented validation after the request.
    
    So the right proof was not:
    > "the backend can try to connect somewhere"
    
    The stronger proof was:
    > "the backend can be made to connect somewhere internal and complete the request successfully under the same image-like constraints the feature expects"
    
    That is exactly what the validation demonstrated.
    
    ---
    
    ## Why This Was Still Worth Reporting
    
    A common reaction to SSRF bugs like this is:
    
    > "the target still has to return an image"
    
    That observation is true but incomplete.
    
    It does not remove the vulnerability.
    
    It just tells you which internal targets are most directly useful.
    
    This issue still enables:
    - backend-origin internal reachability
    - redirect-assisted pivoting into loopback or private-network space
    - interaction with internal image-returning endpoints
    - network trust abuse from the Penpot server position
    
    That is still a real security boundary break.
    
    Especially in self-hosted environments, internal services often exist specifically behind that boundary.
    
    ---
    
    ## Severity and Classification
    
    This issue was ultimately assigned a **High** severity CVSS:
    
    - **CWE-918**: Server-Side Request Forgery (SSRF)
    - **CVSS:**
    ```text
    CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
    ```
    
    That classification makes sense.
    
    The claim is not that an unauthenticated attacker can instantly compromise every Penpot deployment from nothing.
    
    The claim is that any normal authenticated file editor can turn Penpot into a backend request primitive against internal destinations, including redirect-assisted access to loopback and private-network targets.
    
    There was some severity discussion during disclosure, largely around:
    - internal services requiring authentication
    - the fetched content needing to pass image validation
    - exploitation depending on internal infrastructure knowledge
    
    Those are fair constraints to discuss.
    
    But they do not remove the core issue:
    - attacker-controlled URL
    - backend-side request origin
    - redirect following
    - no outbound destination policy in the reviewed path
    
    That is a real and defensible SSRF vulnerability.
    
    ---
    
    ## Fix Analysis
    
    The important fix here is not stricter MIME handling.
    
    The real fix is **outbound destination policy**.
    
    A correct remediation for this class of bug needs to:
    
    1. allow only `http` and `https`
    2. resolve and reject loopback, RFC1918/private, link-local, multicast, unspecified, and metadata-service ranges before connect
    3. re-check every redirect hop against the same policy
    4. consider disabling redirects for this feature or limiting them tightly
    5. add regression coverage for:
       - `localhost`
       - direct private targets
       - redirect-to-private cases
       - DNS rebinding style scenarios
    
    That is the right fix direction because this was not an image parsing bug.
    It was a network trust-boundary bug.
    
    ---
    
    ## Disclosure
    
    This issue was reported privately through GitHub's security reporting flow.
    
    The report included:
    - source-level root cause analysis
    - a strong local validation model
    - redirect-based proof of internal pivoting
    - artifact and log evidence
    - remediation guidance
    
    The maintainers confirmed the issue and began working on a resolution.
    
    The issue was later assigned:
    
    **CVE-2026-45806**
    
    ---
    
    ## What This Bug Actually Teaches
    
    The key lesson here is simple:
    
    > remote media import is an outbound trust boundary, not just a convenience feature
    
    A lot of developers think in terms of:
    - URL accepted
    - request succeeds
    - image passes validation
    - media gets stored
    
    Those are implementation details.
    
    The real security question is:
    
    **where is the backend allowed to connect on behalf of a user?**
    
    If that question is not answered explicitly, features like remote import become SSRF surfaces by default.
    
    This bug also reinforces something important about SSRF review:
    
    - redirects matter
    - content validation is not a substitute for network policy
    - authenticated SSRF is still serious when it crosses into internal trust boundaries
    
    That is the real takeaway.
    
    ---
    
    ## Key Points
    
    - remote image import is a real backend trust boundary
    - authenticated features can still expose serious SSRF
    - redirect following makes outbound fetch paths much more dangerous
    - image-only validation narrows some abuse paths but does not remove SSRF
    - proving a successful internal redirect path is stronger than just showing a failed connection attempt
    - the right fix is outbound destination policy, not cosmetic response validation
    
    ---
    
    ## Final Words
    
    This vulnerability was not about a flashy payload.
    
    It was about asking the right trust-boundary question.
    
    Penpot let an authenticated file editor provide a remote image URL, and the backend trusted that URL farther than it should have.
    The redirect handling did the rest.
    
    That is why this became **CVE-2026-45806**.