CVE-2026-17613: Penpot cross-team file takeover via import-binfile (unpatched in 2.17.2)
Full Disclosuremailing list archivesFrom: Louis Sanchez via Fulldisclosure <full 2026-9-22 18:32:22 Author: seclists.org(查看原文) 阅读量:8 收藏

fulldisclosure logo

Full Disclosure mailing list archives


From: Louis Sanchez via Fulldisclosure <fulldisclosure () seclists org>
Date: Thu, 10 Sep 2026 22:20:54 -0400

Posting this as an update rather than a first disclosure. The advisory
went public on 2026-08-04 with no vendor fix. Penpot has shipped two
releases since then, 2.17.1 and 2.17.2 -- the latter 14 days ago, on
2026-08-27 -- and I re-checked the code this morning: the missing
permission check is still missing in both, and in every release before
them. It was fixed on develop the day after this advisory went public.
That fix has never shipped. Anyone running a released version of
self-hosted Penpot should know that, so here is the whole thing in one
message.


SUMMARY
=======

Penpot's file-import RPC command takes an optional caller-supplied
file-id, meaning "import into this existing file instead of creating a
new one." The handler checks that the caller may edit the project they
named -- a project they own, since they supplied it. It never checks
that the file-id they supplied belongs to them.

So any authenticated user can point that parameter at any file on the
instance and overwrite it. The same operation also re-parents the file
into the attacker's project, which is the part that matters: the
attacker does not merely destroy the victim's design, they end up owning
it. On a default install with open self-registration, the attacker needs
no prior relationship to the target at all.


AFFECTED
========

Product:  Penpot (open-source design and prototyping platform, Kaleidos)
Affected: 1.20 through 2.17.2, self-hosted Community Edition and
          Penpot Cloud. The in-place file-id parameter was introduced in
          1.20 -- the handler's own ::doc/changes metadata records
          "1.20  Add file-id param for in-place import". The CVE record
          was updated 2026-08-27 to state the range as 1.20 through
          2.17.1, the same day 2.17.2 shipped; 2.17.2 is also affected
          and is not yet reflected in the record.
Fixed in: nothing released. Fixed on develop, unreleased, since
          2026-08-05, and present in the 2.18.0 release-candidate line.
          Never backported to a 2.17.x release.

CVE-2026-17613, assigned by CERT/CC as CNA, record state PUBLISHED.
CWE-639 (Authorization Bypass Through User-Controlled Key), CWE-862
(Missing Authorization).


STILL UNPATCHED IN EVERY RELEASE -- FIXED ON DEVELOP, NOT SHIPPED
=================================================================

The published advisory last verified the flaw at develop HEAD on
2026-07-27. Penpot has released 2.17.1 and 2.17.2 since then
(2026-08-17 and 2026-08-27), and 2.17.0 shortly before it (2026-07-22).
None of the three fixes it. Verified today, 2026-09-10:

  backend/src/app/rpc/commands/binfile.clj @ tag 2.17.2

    line  78  (files/check-read-permissions! pool profile-id file-id)
              -- export path, correct check, present
    line 153  (projects/check-edition-permissions! pool profile-id project-id)
              -- import path, checks only the caller's own project
    line 160  (uuid? file-id)
    line 161  (assoc ::bfc/file-id file-id)
              -- import path, the supplied file-id bound unchecked

  same file @ develop 37dab75e (2026-09-10T18:29:44Z)

    line  88  (files/check-read-permissions! cfg profile-id file-id)
              -- export path, correct check, present
    line 166  (projects/check-edition-permissions! pool profile-id project-id)
              -- import path; file-id is no longer a parameter here at
                 all, so there is nothing left to check

The fix exists. It just has not shipped. The day after this advisory
went public, commit 9242556d landed on develop and removed file-id from
import-binfile outright -- gone from the schema, from the handler's
destructuring, and from the audit props. Commit message: ":bug: Close
import-binfile schema and remove file-id parameter (#10994)". The same
change is in the current 2.18.0 release-candidate line (2.18.0-RC5). It
has never been backported to a 2.17.x release.

The 2.17.2 release notes list three bug fixes, one of them a different
security fix (an SVG-exporter command-injection bug, per the vendor's
own changelog) -- so the vendor does put security fixes in this line
when it chooses to. This one is not among them. The code above is
byte-identical to 2.17.0 (2026-07-22) and 2.17.1 (2026-08-17) -- the
same three call sites, the same line numbers, across the last three
releases. I am not going to guess why -- I am saying that today, 105
days after the report, nobody running a released version of Penpot has
the fix that has sat on develop, unreleased, for 36 days.


ROOT CAUSE
==========

What makes this one worth reading is that the correct check is in the
same source file, about seventy lines away, on the other direction of
the same feature.

export-binfile validates read permission on its file-id before handing
anything back:

  (sv/defmethod ::export-binfile
    ...
    [{:keys [::db/pool] :as cfg}
     {:keys [::rpc/profile-id file-id] :as params}]
    (files/check-read-permissions! pool profile-id file-id)   ; <-- yes
    (sse/response (partial export-binfile cfg params)))

import-binfile validates the caller's project and then binds the
caller's file-id straight through:

  (sv/defmethod ::import-binfile
    ...
    [{:keys [::db/pool] :as cfg}
     {:keys [::rpc/profile-id project-id version file-id upload-id]
      :as params}]
    (projects/check-edition-permissions! pool profile-id project-id)
    (let [...
          cfg (cond-> cfg
                (uuid? file-id)
                (assoc ::bfc/file-id file-id))]   ; <-- no check
      ...))

The permission function exists. It is already imported. It is used
correctly a few dozen lines up. The import path just does not call it.

Downstream, the in-place import fetches the target file by id alone with
no ownership predicate on the query, enables overwrite mode, writes the
attacker's content into the victim's file row, and sets that row's
project to the attacker's project. The final update is keyed only on the
attacker-supplied file id.


EXPLOITATION
============

Prerequisite: any authenticated account with a project of its own. On a
default install, self-register.

  POST /api/rpc/command/import-binfile
  Cookie: <attacker session>
  Content-Type: multipart/form-data

    project-id = <a project the attacker owns>   ; checked
    file-id    = <the victim's file>             ; NOT checked
    file       = <a valid single-file .penpot archive>

The obvious objection is that file ids are random UUIDs, so how do you
aim it. In practice they leak through entirely normal use, and two of
the three routes require no mistake by the victim:

  1. Public share links. Clicking Share produces a URL with the file id
     in it in plaintext. People send those to clients and contractors
     all day. Whoever holds the link holds the targeting data.

  2. Shared-library enumeration. Any user who has ever linked a shared
     design-system library can read that library's file id from an
     ordinary library-listing call. No error, no audit trail. This is
     the route that reaches an entire organisation's design system
     rather than one file.

  3. Workspace and viewer URLs embed the file id, so it turns up in
     screenshots, support tickets, Slack pastes and screen shares.

On attack complexity, since it came up during coordination: AC:H is
meant for conditions the attacker cannot arrange unilaterally. Receiving
a share link and listing shared libraries are ordinary actions any
account can take on its own. Complexity stays Low.


IMPACT
======

  - Data destruction. The victim's file contents are replaced with the
    attacker's.

  - Persistent takeover. Penpot resolves file permissions by walking
    file -> project -> team. Re-parenting rewrites that lineage, so the
    attacker becomes the legitimate owner in the eyes of every other
    endpoint, free to open, edit, export, duplicate and re-share the
    victim's design. The file vanishes from the victim's project. They
    cannot reach it to attempt a restore.

  - Library poisoning. If the hijacked file was a shared design library,
    every file in every other team that syncs from it starts pulling
    attacker-controlled components, colours and typography.

  - Cross-team scope. The attacker holds no authorisation on the
    victim's team. The operation moves a resource out of that boundary
    entirely.

Design files are not low-value data. One file can hold an unreleased
product's whole interface, pre-launch brand and marketing assets, or
mockups carrying real customer names in placeholder text. Agencies
routinely run one instance with a team per client and rely on team
separation to keep those clients confidential from each other. That
assumption does not currently hold.

The original report also bundled a lower-severity issue with the same
root pattern: the realtime WebSocket channel lets a client subscribe to
a file or team topic using a client-supplied id with no permission
check, allowing live exfiltration of collaborative edits and
shared-library changes for any file whose id you know. Same mistake --
trusting a client-supplied identifier without re-checking it. One
disciplined patch closes both.


ON THE SCORE
============

The CVE record carries 7.5 High:

  CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

That is also the number on my own advisory. I publish the assigned score
rather than mine, because the CVE record is what the ecosystem actually
consumes and a mismatched headline number reads as inflation. It is also
the number I disagree with, and I would rather argue that in the open
than quietly ship a different one.

C:N/I:N describes the vulnerable endpoint honestly enough if you read
the operation as "the victim's file is destroyed." But the file is not
destroyed. It is moved, intact, into the attacker's project, where they
can read and export it. That is a confidentiality loss. Its contents are
then attacker-controlled, and if it was a shared library that content
propagates into other teams' files. That is an integrity loss with a
blast radius past the original resource. Scoring it A:H only measures
what the victim stopped having, not what the attacker started having.

S:U has the same problem. The operation carries a resource across a team
boundary the attacker was never authorised for; calling that scope
unchanged treats Penpot's file store as one undifferentiated pool, which
is precisely the assumption the product's team model tells customers is
false.

My pre-assignment score was 9.9. Take that as one researcher's read, not
as a correction to the record -- but if you are triaging this from the
7.5 alone, triage the impact section above instead. Worth more than
either number for that purpose: the CISA ADP Vulnrichment enrichment on
the same record carries an SSVC assessment of exploitation
proof-of-concept, technical impact partial, automatable yes. I wrote the
general version of this argument up here, alongside the same pattern in
CVE-2026-16751:

  https://vokecyber.com/blog/when-cvss-scores-the-endpoint-not-the-loss


MITIGATIONS
===========

No fixed release to upgrade to, so, in order of value:

  1. Disable open self-registration. Does not fix the bug. Removes the
     sign-up-and-attack path and takes the score to 6.5, since the
     attacker then needs an account somebody gave them.

  2. Treat every account on the instance as having write access to every
     file on it, and plan accordingly. If you rely on team separation
     for client confidentiality, consider separate instances for
     genuinely sensitive work until this is fixed.

  3. Audit for prior exploitation. Look for file rows whose owning
     project changed with no corresponding user action, and for edits
     written by a profile that is not a member of the file's team. An
     in-place import does not leave a normal change row behind, so the
     signal to chase is a file whose project lineage no longer matches
     the accounts that historically edited it.

  4. Keep backups outside Penpot. Because the victim loses access to the
     file entirely, there is no in-product recovery path. Periodic
     export-binfile snapshots stored elsewhere are the practical
     fallback.


THE FIX
=======

Two assertions, both cheap:

  1. When an in-place import is requested, require edit permission on
     the supplied file-id, not just on the caller's project. The
     function to call is the one export-binfile already uses.

  2. Separately, reject any in-place import that would move the file
     into a project other than the one it currently belongs to.

The second matters more than it looks. It kills the re-parenting leg on
its own, so if a future refactor ever loosens the per-file check, the
bug cannot come back as a takeover.

That is not what the develop fix actually does. Commit 9242556d skips
both assertions and deletes file-id from import-binfile instead -- no
in-place import, no target file, nothing left to authorize. Fewer moving
parts, same result: no unchecked caller-supplied file-id, no
re-parenting path. Whether in-place import ever comes back with the
checks above, or stays gone, either one closes this.


ON THE WITHHELD PROOF-OF-CONCEPT
================================

The request shape above is the whole mechanism, and it is enough to
audit your own instance or write your own test. The drop-in script and
the payload recipe are held back until a fixed release ships. I am aware
that is not this list's default preference, and I am not going to
pretend it is a principled universal position -- it is a judgement about
this specific case: no released patch exists, no version to upgrade to,
and the mitigations above are configuration changes rather than fixes.
If the vendor backports the fix to a release, or if it becomes clear
that withholding is protecting nobody, the rest goes out.


TIMELINE
========

  2026-05-27  Found during a source review of the Penpot backend,
              confirmed against develop HEAD.
  2026-05-28  Reported privately via GitHub's advisory process and to
              the vendor's published security contact.
  2026-06-18  No vendor response after 21 days. Confirmed no silent
              patch had landed. Escalated to CERT/CC.
  2026-06-29  CERT/CC raised the attack-complexity question; answered
              same day.
  2026-07-06  CVE-2026-17613 assigned; CERT/CC case VU#241166 opened.
  2026-07-27  Re-verified vulnerable at develop HEAD; proof-of-concept
              package provided to CERT/CC.
  2026-08-04  Public disclosure. No vendor fix.
  2026-08-05  Fix merged to develop (commit 9242556d): file-id removed
              from import-binfile entirely. Never backported to a
              release.
  2026-08-17  Penpot 2.17.1 released. Not fixed.
  2026-08-27  Penpot 2.17.2 released. Not fixed.
  2026-09-10  Re-verified vulnerable at 2.17.2 (latest release) and
              fixed at develop HEAD (37dab75e) and in 2.18.0-RC5. This
              post.

105 days from report to this post. Seven releases have shipped since the
report -- 2.15.4, 2.16.0, 2.16.1, 2.16.2, 2.17.0, 2.17.1, 2.17.2 -- and
none of them carry the fix, though the fix itself has now existed,
unreleased, on develop for 36 of those days.

A note on identifiers, so nobody wastes time clicking: CERT/CC is the
CNA on this CVE and opened case VU#241166, but no public vulnerability
note was ever published, so that VU number does not resolve. Likewise
the GitHub advisory draft GHSA-8qqw-wm58-45v7 was the private reporting
channel and was never published by the vendor, so it 404s at
github.com/advisories. Both are real case identifiers, neither is a
citable public page. The CVE record is the authoritative reference.


REFERENCES
==========

CVE record
  https://www.cve.org/CVERecord?id=CVE-2026-17613

Full advisory
  https://vokecyber.com/research/cve-2026-17613-penpot-cross-team-file-takeover

Disclosure narrative
  https://vokecyber.com/blog/cve-2026-17613-penpot-cross-team-file-takeover

Vendor
  https://penpot.app/
  https://github.com/penpot/penpot

If anyone reading this has a working line to the Penpot maintainers, I
would rather have this fixed than be right about it. Happy to answer
questions on-list.

--
Louis Sanchez
Voke Cyber -- https://vokecyber.com
Research index: https://vokecyber.com/research
info () vokecyber com
OSCP, OSWA, CISSP, CCSK
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/


Current thread:

  • CVE-2026-17613: Penpot cross-team file takeover via import-binfile (unpatched in 2.17.2) Louis Sanchez via Fulldisclosure (Sep 22)

文章来源: https://seclists.org/fulldisclosure/2026/Sep/47
如有侵权请联系:admin#unsafe.sh