Summary
Plane's V2 asset subsystem contains two related authorization flaws that break workspace isolation for any authenticated user.
First, WorkspaceFileAssetEndpoint does not verify that the caller belongs to the target workspace before creating, reading, patching, or deleting assets under /api/assets/v2/workspaces/<slug>/.... Second, DuplicateAssetEndpoint authorizes only the destination workspace and copies any uploaded source asset by UUID without checking whether the caller is allowed to access the source workspace.
I validated this locally on Plane Community Edition 1.2.3, repo commit 1faf06c7553d2bbce59634ae96fb498495c46d62, by using one normal user in workspace Alpha and one normal user in workspace Bravo. Bravo was able to download Alpha's private asset, duplicate it into Bravo's own workspace, delete Alpha's original asset, and overwrite Alpha's workspace logo with attacker-controlled content.
Details
Tested target:
- Plane Community Edition
1.2.3
- Repo commit
1faf06c7553d2bbce59634ae96fb498495c46d62
- Tested on
2026-03-23
Root cause 1: missing authorization in workspace asset routes
apps/api/plane/app/urls/asset.py:50-56 exposes workspace-level asset routes to WorkspaceFileAssetEndpoint.
apps/api/plane/app/views/asset/v2.py:314 creates assets after only resolving Workspace.objects.get(slug=slug).
apps/api/plane/app/views/asset/v2.py:379 patches assets using only FileAsset.objects.get(id=asset_id, workspace__slug=slug).
apps/api/plane/app/views/asset/v2.py:400 deletes assets using only FileAsset.objects.get(id=asset_id, workspace__slug=slug).
apps/api/plane/app/views/asset/v2.py:409 returns a presigned download URL using only FileAsset.objects.get(id=asset_id, workspace__slug=slug).
- These handlers do not enforce workspace membership or role checks before allowing read/write operations against another workspace's assets.
Root cause 2: source asset not authorized during duplication
apps/api/plane/app/urls/asset.py:100-101 maps /api/assets/v2/workspaces/<slug>/duplicate-assets/<asset_id>/ to DuplicateAssetEndpoint.
apps/api/plane/app/views/asset/v2.py:736-780 authorizes the destination workspace only.
- The source asset is loaded at
apps/api/plane/app/views/asset/v2.py:755 with:
original_asset = FileAsset.objects.filter(id=asset_id, is_uploaded=True).first()
- There is no check that the caller belongs to, or is otherwise authorized for, the source workspace that owns
asset_id.
PoC
I am attaching a PoC bundle with a local end-to-end script, raw HTTP evidence, and hash proofs. Minimal reproduction steps are:
- Start a fresh local Plane instance from commit
1faf06c7553d2bbce59634ae96fb498495c46d62.
- Create two normal users and two unrelated workspaces:
- Alpha user in workspace
alpha-20260323072017
- Bravo user in workspace
bravo-20260323072017
- As Alpha, create a project issue and upload a private asset through the project asset flow. In my validated run the private asset ID was:
6ed6ed62-d1b2-4399-8220-336c01b7d72c
- As Bravo, request:
GET /api/assets/v2/workspaces/alpha-20260323072017/6ed6ed62-d1b2-4399-8220-336c01b7d72c/
- Plane returned
302 Found with a presigned download URL for Alpha's asset.
- The downloaded SHA-256 matched Alpha's original private asset exactly:
- original:
0d4d070f550ba59ba6b30bee62343cf68ea221af7034f131f72f6409cf5a598e
- unauthorized read:
0d4d070f550ba59ba6b30bee62343cf68ea221af7034f131f72f6409cf5a598e
- As Bravo, duplicate Alpha's asset into Bravo's own workspace:
POST /api/assets/v2/workspaces/bravo-20260323072017/duplicate-assets/6ed6ed62-d1b2-4399-8220-336c01b7d72c/
- Plane returned
200 OK and created:
- duplicated asset ID
72d51497-ccc1-4546-ba14-28fae5d37dbb
- The duplicated file's SHA-256 also matched Alpha's original asset exactly:
0d4d070f550ba59ba6b30bee62343cf68ea221af7034f131f72f6409cf5a598e
- As Bravo, delete Alpha's original asset:
DELETE /api/assets/v2/workspaces/alpha-20260323072017/6ed6ed62-d1b2-4399-8220-336c01b7d72c/
- Plane returned
204 No Content
- Alpha then received
404 Not Found when fetching the original asset
- As Bravo, overwrite Alpha's workspace logo by creating a
WORKSPACE_LOGO asset against Alpha's workspace via:
POST /api/assets/v2/workspaces/alpha-20260323072017/
- upload attacker-controlled content
PATCH /api/assets/v2/workspaces/alpha-20260323072017/<new_asset_id>/
- Alpha's workspace metadata then pointed to attacker-controlled logo asset
c1032f06-3cf5-4f7e-b139-e6976d8c567d
- The downloaded logo SHA-256 matched the attacker payload exactly:
- expected:
b9d1ef1de88d61bf55dd18055839bacacb832a752e17a7312547f641113d0e7b
- observed:
b9d1ef1de88d61bf55dd18055839bacacb832a752e17a7312547f641113d0e7b
Impact
This is a high-severity cross-workspace authorization bypass affecting any multi-workspace Plane deployment where an attacker can authenticate as a normal user.
Validated impact:
- cross-workspace disclosure of non-public uploaded assets
- cross-workspace duplication of non-public assets into an attacker-controlled workspace
- cross-workspace deletion of victim assets
- cross-workspace workspace-logo overwrite / visual defacement
The vulnerable workspace asset endpoint also accepts multiple entity types beyond workspace logos, so the same authorization gap likely affects additional asset-backed objects as well.
Mitigation
I recommend fixing this in both code paths, because patching only one still leaves cross-workspace asset abuse reachable.
-
Enforce workspace authorization on every WorkspaceFileAssetEndpoint action before any object lookup or mutation.
POST /api/assets/v2/workspaces/<slug>/
GET /api/assets/v2/workspaces/<slug>/<asset_id>/
PATCH /api/assets/v2/workspaces/<slug>/<asset_id>/
DELETE /api/assets/v2/workspaces/<slug>/<asset_id>/
These handlers should require that the caller is an authorized member of the target workspace with sufficient permissions for the requested action.
-
Authorize the referenced entity, not just the workspace slug.
For asset creation, the server should verify that the caller is allowed to act on the specific target object represented by entity_type and entity_identifier before creating the FileAsset. For example, a caller should not be able to create or finalize a WORKSPACE_LOGO asset for another workspace, or a project-cover asset for a project they do not control.
-
In DuplicateAssetEndpoint, authorize both sides of the operation.
The current implementation checks only the destination workspace. It should also verify that the caller is authorized to access the source asset in its original workspace or project before allowing the copy. A direct fix is to scope the source lookup through an authorization-aware queryset rather than FileAsset.objects.filter(id=asset_id, is_uploaded=True).first().
-
Avoid direct object access by attacker-controlled identifiers.
Asset UUIDs and workspace slugs should not be sufficient by themselves to perform read/write operations. All asset fetches should be constrained by an authorization filter tied to the current user.
-
Add regression tests for tenant isolation.
Add tests that create two unrelated workspaces and verify that a user from workspace B cannot:
- read workspace-level assets from workspace A
- duplicate uploaded assets from workspace A
- patch or finalize workspace A assets
- delete workspace A assets
- overwrite workspace A branding assets
-
Review other V2 asset flows for the same pattern.
Because the vulnerable endpoint supports multiple entity contexts, the same authorization mistake may affect other asset-backed objects such as project covers, user images, issue content, page content, and comment content. A full audit of all asset routes is recommended.
Attachment
plane-cross-workspace-asset-authz-poc-20260323.zip
Summary
Plane's V2 asset subsystem contains two related authorization flaws that break workspace isolation for any authenticated user.
First,
WorkspaceFileAssetEndpointdoes not verify that the caller belongs to the target workspace before creating, reading, patching, or deleting assets under/api/assets/v2/workspaces/<slug>/.... Second,DuplicateAssetEndpointauthorizes only the destination workspace and copies any uploaded source asset by UUID without checking whether the caller is allowed to access the source workspace.I validated this locally on Plane Community Edition
1.2.3, repo commit1faf06c7553d2bbce59634ae96fb498495c46d62, by using one normal user in workspace Alpha and one normal user in workspace Bravo. Bravo was able to download Alpha's private asset, duplicate it into Bravo's own workspace, delete Alpha's original asset, and overwrite Alpha's workspace logo with attacker-controlled content.Details
Tested target:
1.2.31faf06c7553d2bbce59634ae96fb498495c46d622026-03-23Root cause 1: missing authorization in workspace asset routes
apps/api/plane/app/urls/asset.py:50-56exposes workspace-level asset routes toWorkspaceFileAssetEndpoint.apps/api/plane/app/views/asset/v2.py:314creates assets after only resolvingWorkspace.objects.get(slug=slug).apps/api/plane/app/views/asset/v2.py:379patches assets using onlyFileAsset.objects.get(id=asset_id, workspace__slug=slug).apps/api/plane/app/views/asset/v2.py:400deletes assets using onlyFileAsset.objects.get(id=asset_id, workspace__slug=slug).apps/api/plane/app/views/asset/v2.py:409returns a presigned download URL using onlyFileAsset.objects.get(id=asset_id, workspace__slug=slug).Root cause 2: source asset not authorized during duplication
apps/api/plane/app/urls/asset.py:100-101maps/api/assets/v2/workspaces/<slug>/duplicate-assets/<asset_id>/toDuplicateAssetEndpoint.apps/api/plane/app/views/asset/v2.py:736-780authorizes the destination workspace only.apps/api/plane/app/views/asset/v2.py:755with:original_asset = FileAsset.objects.filter(id=asset_id, is_uploaded=True).first()asset_id.PoC
I am attaching a PoC bundle with a local end-to-end script, raw HTTP evidence, and hash proofs. Minimal reproduction steps are:
1faf06c7553d2bbce59634ae96fb498495c46d62.alpha-20260323072017bravo-202603230720176ed6ed62-d1b2-4399-8220-336c01b7d72cGET /api/assets/v2/workspaces/alpha-20260323072017/6ed6ed62-d1b2-4399-8220-336c01b7d72c/302 Foundwith a presigned download URL for Alpha's asset.0d4d070f550ba59ba6b30bee62343cf68ea221af7034f131f72f6409cf5a598e0d4d070f550ba59ba6b30bee62343cf68ea221af7034f131f72f6409cf5a598ePOST /api/assets/v2/workspaces/bravo-20260323072017/duplicate-assets/6ed6ed62-d1b2-4399-8220-336c01b7d72c/200 OKand created:72d51497-ccc1-4546-ba14-28fae5d37dbb0d4d070f550ba59ba6b30bee62343cf68ea221af7034f131f72f6409cf5a598eDELETE /api/assets/v2/workspaces/alpha-20260323072017/6ed6ed62-d1b2-4399-8220-336c01b7d72c/204 No Content404 Not Foundwhen fetching the original assetWORKSPACE_LOGOasset against Alpha's workspace via:POST /api/assets/v2/workspaces/alpha-20260323072017/PATCH /api/assets/v2/workspaces/alpha-20260323072017/<new_asset_id>/c1032f06-3cf5-4f7e-b139-e6976d8c567db9d1ef1de88d61bf55dd18055839bacacb832a752e17a7312547f641113d0e7bb9d1ef1de88d61bf55dd18055839bacacb832a752e17a7312547f641113d0e7bImpact
This is a high-severity cross-workspace authorization bypass affecting any multi-workspace Plane deployment where an attacker can authenticate as a normal user.
Validated impact:
The vulnerable workspace asset endpoint also accepts multiple entity types beyond workspace logos, so the same authorization gap likely affects additional asset-backed objects as well.
Mitigation
I recommend fixing this in both code paths, because patching only one still leaves cross-workspace asset abuse reachable.
Enforce workspace authorization on every
WorkspaceFileAssetEndpointaction before any object lookup or mutation.POST /api/assets/v2/workspaces/<slug>/GET /api/assets/v2/workspaces/<slug>/<asset_id>/PATCH /api/assets/v2/workspaces/<slug>/<asset_id>/DELETE /api/assets/v2/workspaces/<slug>/<asset_id>/These handlers should require that the caller is an authorized member of the target workspace with sufficient permissions for the requested action.
Authorize the referenced entity, not just the workspace slug.
For asset creation, the server should verify that the caller is allowed to act on the specific target object represented by
entity_typeandentity_identifierbefore creating theFileAsset. For example, a caller should not be able to create or finalize aWORKSPACE_LOGOasset for another workspace, or a project-cover asset for a project they do not control.In
DuplicateAssetEndpoint, authorize both sides of the operation.The current implementation checks only the destination workspace. It should also verify that the caller is authorized to access the source asset in its original workspace or project before allowing the copy. A direct fix is to scope the source lookup through an authorization-aware queryset rather than
FileAsset.objects.filter(id=asset_id, is_uploaded=True).first().Avoid direct object access by attacker-controlled identifiers.
Asset UUIDs and workspace slugs should not be sufficient by themselves to perform read/write operations. All asset fetches should be constrained by an authorization filter tied to the current user.
Add regression tests for tenant isolation.
Add tests that create two unrelated workspaces and verify that a user from workspace B cannot:
Review other V2 asset flows for the same pattern.
Because the vulnerable endpoint supports multiple entity contexts, the same authorization mistake may affect other asset-backed objects such as project covers, user images, issue content, page content, and comment content. A full audit of all asset routes is recommended.
Attachment
plane-cross-workspace-asset-authz-poc-20260323.zip