The Import That Took the File With It: CVE-2026-17613 in Penpot
Most access-control bugs let an attacker touch something that is not theirs. The one we found in Penpot goes a step further: it does not just let you write to someone else's file, it moves that file into your account on the way out. When the request finishes, the victim's design is gone from their project, sitting in yours, filled with your content, and as far as every other part of the system is concerned, it was always yours.
Penpot is the open-source alternative to Figma, run both as a hosted service and self-hosted by tens of thousands of teams. Work is organized into files, inside projects, inside teams. The entire point of that structure is that one team's designs are private to that team. This bug dissolves that boundary from any account on the instance.
The short version
CVE-2026-17613 is a cross-team file takeover (CWE-639/CWE-862) in Penpot. Its file-import API accepts an optional file-id parameter meaning "import into this existing file instead of creating a new one." The server checks that you can edit your own project, which you obviously can. It never checks that the file-id you supplied belongs to you. So any logged-in user can point that parameter at any file on the entire instance, overwrite it, and have it re-parented into their own project. CVSS 9.9, rising to 10.0 on instances with open self-registration. There is no patch.
No fix available
This is being published after CERT/CC coordination (VU#241166) because the vendor did not respond and there is still no patched release. The flaw was re-verified live at develop HEAD on July 27, 2026. If you self-host Penpot, the mitigations near the end of this post are what you have to work with right now. The drop-in exploit script and the payload recipe are being held back until a fix ships.
One parameter nobody checked
Importing a Penpot file goes through the import-binfile command. Normally you hand it a project and an archive, and it creates a new file. But it also takes an optional file-id, and when you supply one it does an "in-place" import: instead of creating a file, it overwrites the one you named.
The handler validates edit permission on the project-id you sent. That check passes trivially, because you sent your own project. The file-id is bound straight through and used with no permission check at all.
What makes this one land differently is what sits a few dozen lines away in the same source file. The export-binfile command — the other half of the same feature — validates read permission on its file-id before handing anything back. The permission function exists. It is already imported into that namespace. It is used correctly on the export path. The import path just does not call it.
That is not a design disagreement or a subtle contract mismatch between layers. It is one missing line, in a file that demonstrates on the adjacent function exactly what the missing line should look like.
Why it is takeover and not just vandalism
Here is where it stops being an ordinary write-side IDOR. The in-place import fetches the target file by id alone, with no ownership predicate on the query. It enables overwrite mode. It writes your content into the victim's file row. And then it sets that file's project to your project. The final database update is keyed only on the file id you supplied.
Penpot resolves file permissions by walking from the file, to its project, to its team. Rewrite the project pointer and you have not bypassed the permission model — you have satisfied it. Every other endpoint in the application now agrees that the file is yours. You can open it, edit it, export it, duplicate it, share it.
The victim, meanwhile, cannot reach it at all. It is not in their project anymore. There is no in-product restore path, because restoring would require access to a file they no longer own.
Most write-side IDORs end at "the attacker corrupted your data." This one ends at "the attacker has your data, and you do not."
"But file ids are random"
This was the first objection, and it is the right one to raise. Penpot file identifiers are UUIDs. You are not going to guess one. So how does an attacker aim this?
They do not have to. File ids leak through completely ordinary use, and two of the three routes require no mistake from the victim at all.
Public share links. When a designer clicks Share, Penpot hands them a URL with the file id sitting in it in plaintext. People send those to clients, contractors, and coworkers constantly — that is the entire purpose of the feature. Anyone who has ever received a share link already holds what the attack needs.
Shared-library enumeration. Any user who has linked a shared design-system library can read that library file's id from a normal library-listing call. No error, no unusual request, nothing in a log that looks wrong. This is the worst one, because a design system is not one file — it is the file that every other file in the organization syncs from.
Workspace and viewer URLs. The file id is in the address bar. It is therefore in every screenshot, every support ticket, every Slack paste, every issue screencap, every screen share.
So the attack is: obtain an id through one of those, then send a single import call with project-id set to a project you own and file-id set to the victim's file, with a valid Penpot archive as the payload. That is the whole exploit.
The complexity argument, and why it failed
During coordination, CERT/CC raised a fair question: given that you need a file id you cannot guess, should attack complexity be scored High rather than Low? That single letter is the difference between a 9.9 and something considerably tamer, so it was worth settling properly.
The distinction that resolves it is what AC:H is actually for. High complexity is meant for conditions the attacker cannot arrange on their own — a race window they have to win, a specific memory layout, a victim who must independently take some action at the right moment. It is not meant for "the attacker has to do a normal thing first."
Receiving a share link and listing the shared libraries your account can see are both ordinary actions any account can take unilaterally, at any time, with no cooperation from anyone. Nothing has to go right by chance. The complexity stays Low.
I am spelling this out because scoring your own findings honestly is harder than finding them, and the temptation runs both directions. The useful test is not "was this hard for me?" It is "can the attacker create this condition whenever they want?"
What operators can do right now
There is no patched release to upgrade to, so the honest guidance is about shrinking exposure and looking for evidence, not closing the hole.
The single most effective step is to disable open self-registration. It does not fix anything, but it removes the sign-up-and-attack path, which is the difference between "anyone on the internet" and "someone you already gave an account to." In CVSS terms it takes the score from 10.0 down to 9.9, and in practical terms it is the difference that matters most.
Beyond that: treat every account on the instance as having write access to every file on it. If you are an agency running one Penpot instance with a team per client, and you rely on team separation to keep those clients confidential from each other, that assumption does not currently hold. Genuinely sensitive client work may warrant its own instance until this is fixed.
For detection, look for file rows whose owning project changed without a 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.
And keep your own backups. Because the victim loses access to the file entirely, in-product recovery is not an option. Periodic exports stored outside Penpot are the practical fallback.
The disclosure
- May 27, 2026 Found during a source review of the Penpot backend and confirmed against develop HEAD.
- May 28, 2026 Reported privately through GitHub's advisory process (GHSA-8qqw-wm58-45v7) and to the vendor's published security contact.
- June 18, 2026 No vendor response after 21 days. Confirmed no silent patch had landed, and reported to CERT/CC for coordination.
- June 29, 2026 CERT/CC raised the attack-complexity question. Answered the same day.
- July 6, 2026 CVE-2026-17613 assigned and CERT/CC case VU#241166 opened.
- July 27, 2026 Re-verified still vulnerable at develop HEAD. Proof-of-concept package provided to CERT/CC.
- August 4, 2026 Public disclosure, with no vendor fix available.
The broader lesson
The thing worth taking from this one is not "check your ids." Everyone knows to check their ids. It is that the export path proves the team knew. The correct check was written, by the same people, in the same file, for the same resource, on the other direction of the same feature. The knowledge was there. It just did not get applied on both paths.
That is what makes symmetric features worth reviewing as pairs. Import and export. Create and update. Single-record and bulk. Read and write. When a codebase does something correctly in one direction, the fastest question you can ask is whether it does the same thing in the other — because the presence of the right check nearby is not evidence that the check is everywhere. Sometimes it is just evidence of what the missing one should have looked like.
A scanner cannot ask that question. It has no concept that these two functions are two halves of one idea, or that a permission call present in one and absent in the other is a finding rather than a stylistic difference. That comparison requires someone who understands what the feature is supposed to mean.
For the full technical detail — the handler behavior, the CVSS vector rationale, the impact breakdown, and the operator audit steps — see the full advisory.
The check that exists proves nothing about the one that does not
The most dangerous authorization gaps sit right next to correct code. They surface when someone reads both halves of a feature and asks why only one of them validates its input. That is the same question we ask when testing web applications and APIs for clients across the Charlotte, NC area and nationwide.
Get a Quote