
Plane’s V2 asset subsystem trusted workspace slugs and asset UUIDs without enforcing the right membership checks, which let one authenticated user read, copy, delete, and overwrite assets in other workspaces.
Plane’s V2 asset subsystem trusted workspace slugs and asset UUIDs without enforcing the right membership checks, which let one authenticated user read, copy, delete, and overwrite assets in other workspaces.
I found this issue while reviewing Plane, the open-source project management platform, with a very specific question in mind:
Do the V2 asset endpoints actually enforce workspace boundaries, or do they trust attacker-supplied workspace slugs and asset IDs too far?
In this case, the answer was no.
Plane’s V2 asset subsystem exposed two related authorization flaws that broke workspace isolation for any authenticated user:
That made cross-workspace asset abuse possible.
In my validated PoC, one normal user in workspace Bravo was able to:
That issue was later assigned CVE-2026-46558.
Plane: Plane on GitHub
CVE: CVE-2026-46558
This affected Plane, which on its official site is presented as being used by 50,000+ teams across the globe. Plane also highlights strong open-source adoption, including 46,000+ GitHub stars and 1,000,000+ Docker pulls, and showcases organizations such as Tencent, Accenture, Microsoft, and Amazon.
authenticated attacker in workspace B → workspace-level V2 asset route trusts target workspace slug and asset UUID without proper membership checks → presigned read / patch / delete against workspace A assets + duplicate-assets source lookup trusts uploaded source UUID → cross-workspace disclosure, copying, deletion, and branding overwrite
Plane is an open-source project management platform used to manage:
That means its asset subsystem sits on a real trust boundary.
The important question here was not whether Plane supports uploads.
The real question was:
Does Plane enforce workspace isolation when one authenticated user references assets owned by another workspace?
In this case, it did not.
A lot of multi-tenant application reviews focus first on obvious admin endpoints or direct settings updates.
That misses a very common and very real bug class:
secondary object access through shared file or asset subsystems
Asset systems are easy to get wrong because they often combine:
That is exactly the kind of place where tenant boundaries silently weaken.
This issue was not about storage corruption. It was not about S3 itself. It was not about upload MIME handling.
It was an authorization boundary failure:
That is enough to create a real vulnerability.
I did not approach Plane by blindly fuzzing random endpoints or guessing at UUIDs with no model.
The stronger approach was to identify the most promising isolation boundary first.
For Plane, that was the V2 asset subsystem.
Why?
Because a shared asset system becomes dangerous when:
That was the right boundary to inspect.
And it was exactly where the bug lived.
This was really two related authorization failures in the same subsystem.
The workspace-level asset routes were exposed through:
apps/api/plane/app/urls/asset.py:50-56The vulnerable handlers were in:
apps/api/plane/app/views/asset/v2.py:314apps/api/plane/app/views/asset/v2.py:379apps/api/plane/app/views/asset/v2.py:400apps/api/plane/app/views/asset/v2.py:409The problem was simple.
WorkspaceFileAssetEndpoint accepted a workspace slug and asset UUID, then directly resolved objects like:
workspace = Workspace.objects.get(slug=slug)
and:
asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug)
without first enforcing that the caller was actually an authorized member of that target workspace.
That meant the endpoint could still:
for another workspace’s objects.
The duplicate-assets route was mapped through:
apps/api/plane/app/urls/asset.py:100-101The vulnerable logic was in:
apps/api/plane/app/views/asset/v2.py:736-780The destination workspace had an authorization decorator. But the source asset lookup did not.
The source object was loaded with:
original_asset = FileAsset.objects.filter(id=asset_id, is_uploaded=True).first()
That meant the caller only needed:
There was no check that the caller belonged to the source workspace that actually owned that asset.
That is the whole second bug.
The important distinction is cross-workspace impact.
A lot of authorization bugs get minimized as:
“it still requires login”
That misses the point.
The real question is not:
“Is the caller authenticated?”
The real question is:
“Is the caller authorized for the specific workspace and specific asset being acted on?”
In Plane, that answer was no.
That turns what might look like ordinary object handling into a real multi-tenant security issue.
There is a clear difference between:
This issue was firmly the second case.
I validated the issue locally against Plane Community Edition 1.2.3 using two ordinary users in two unrelated workspaces:
alpha-20260323072017bravo-20260323072017I used Alpha to create a legitimate private uploaded asset in a project issue.
The validated private asset ID in my run was:
6ed6ed62-d1b2-4399-8220-336c01b7d72c
As Bravo, I requested:
GET /api/assets/v2/workspaces/alpha-20260323072017/6ed6ed62-d1b2-4399-8220-336c01b7d72c/
Plane returned:
HTTP/1.1 302 Found
with a presigned download URL for Alpha’s asset.
The downloaded file hash matched Alpha’s original private asset exactly:
original: 0d4d070f550ba59ba6b30bee62343cf68ea221af7034f131f72f6409cf5a598e
unauthorized read: 0d4d070f550ba59ba6b30bee62343cf68ea221af7034f131f72f6409cf5a598e
That proved the read path crossed workspace boundaries successfully.
As Bravo, I then requested: