Skip to content

Cross-workspace asset authorization bypass lets any authenticated user read, copy, delete, and overwrite assets in other Plane workspaces

High
sriramveeraghanta published GHSA-qw87-v5w3-6vxx May 15, 2026

Package

makeplane/plane

Affected versions

v1.2.3

Patched versions

1.3.1

Description

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:

  1. Start a fresh local Plane instance from commit 1faf06c7553d2bbce59634ae96fb498495c46d62.
  2. Create two normal users and two unrelated workspaces:
    • Alpha user in workspace alpha-20260323072017
    • Bravo user in workspace bravo-20260323072017
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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.

  1. 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.
  2. 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.

  3. 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().

  4. 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.

  5. 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
  6. 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

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L

CVE ID

CVE-2026-46558

Weaknesses

Authorization Bypass Through User-Controlled Key

The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data. Learn more on MITRE.

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Credits