Skip to content

[WEB-8066] fix: scope workspace asset get/patch/delete to project membership#9372

Merged
dheeru0198 merged 3 commits into
previewfrom
web-8066/workspace-asset-project-scope
Jul 14, 2026
Merged

[WEB-8066] fix: scope workspace asset get/patch/delete to project membership#9372
dheeru0198 merged 3 commits into
previewfrom
web-8066/workspace-asset-project-scope

Conversation

@mguptahub

@mguptahub mguptahub commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

WorkspaceFileAssetEndpoint (apps/api/plane/app/views/asset/v2.py) is authorized at the WORKSPACE level. Its get, patch, and delete fetch FileAsset.objects.get(id=asset_id, workspace__slug=slug) with no project-membership check, so any workspace member or guest could download, mark-uploaded, or delete a project-bound asset (issue attachment/description, comment description, page description) belonging to a project they are not a member of.

This is an incomplete fix of the GHSA-qw87 asset-IDOR cluster.

Advisories: GHSA-h7mc-p9mm-2r4w (dup: GHSA-cjph-cgm5-8pw8) · MEDIUM.

Fix

  • New project_membership_denied(request, asset) helper: for project-bound assets (asset.project_id is not None), require an active ProjectMember of asset.project_id, otherwise 403.
  • Workspace-level entity types (WORKSPACE_LOGO, USER_AVATAR, USER_COVER) have project_id = NULL and remain accessible to any workspace member.
  • Guard is invoked in get, patch, and delete immediately after the asset fetch — before the is_uploaded check / any mutation — so a non-member receives a uniform 403 and cannot probe upload state.
  • Mirrors the strictest adjacent endpoint, ProjectAssetEndpoint (level="PROJECT").

Tests

plane/tests/contract/app/test_workspace_file_asset_project_scope_app.py:

  • get/patch/delete on a project asset by a non-project-member → 403 (and no presigned URL minted / no mutation).
  • Positive control: active project member can still download.
  • Exemption control: workspace-level asset stays accessible to any workspace member.

Fail-before verified: with the fix stashed, the three denied cases return 302/204/204 (the vulnerable behavior); positive + exemption controls still pass.

Gates: ruff ✅ · manage.py check ✅ · sibling test_generic_asset.py regression ✅.

Follow-up (out of scope)

Sibling workspace-scoped endpoints share the same shape and warrant a separate audit: AssetRestoreEndpoint, AssetCheckEndpoint, DuplicateAssetEndpoint, WorkspaceAssetDownloadEndpoint.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened access controls for project-bound workspace file assets: only active project members can view, update, or delete them.
    • Unauthorized requests now return 403 and are blocked before any download link is generated or any asset changes occur.
    • Workspace-level assets (no project scope) remain accessible to eligible workspace members.
  • Tests
    • Added contract tests covering project scoping denial/allow scenarios, including presigned URL generation and ensuring no changes on forbidden PATCH/DELETE.

…bership

WorkspaceFileAssetEndpoint is authorized at the WORKSPACE level, so any
workspace member/guest could reach get/patch/delete for a project-bound
asset (issue attachment/description, comment description, page description)
of a project they are not a member of — an incomplete fix of the GHSA-qw87
asset-IDOR cluster (GHSA-h7mc-p9mm-2r4w / GHSA-cjph-cgm5-8pw8).

Add project_membership_denied(): for project-bound assets (project_id set)
require an active ProjectMember of the asset's project, else 403. Workspace-
level entity types (WORKSPACE_LOGO, USER_AVATAR, USER_COVER) have project_id
NULL and remain accessible to any workspace member. Mirrors ProjectAssetEndpoint
(level=PROJECT). Guard runs before the is_uploaded check / mutation so a
non-member gets a uniform 403 and cannot probe upload state.

Contract regression tests cover denied get/patch/delete for a non-project
member, the positive project-member path, and the workspace-level exemption;
fail-before verified.

Co-authored-by: Plane AI <noreply@plane.so>
Copilot AI review requested due to automatic review settings July 8, 2026 08:24
@makeplane

makeplane Bot commented Jul 8, 2026

Copy link
Copy Markdown

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1097de00-030e-4d01-b595-705ff745e14b

📥 Commits

Reviewing files that changed from the base of the PR and between 90b6416 and be67aab.

📒 Files selected for processing (1)
  • apps/api/plane/app/views/asset/v2.py

📝 Walkthrough

Walkthrough

Adds project-level authorization to WorkspaceFileAssetEndpoint. Project-bound assets now require active project membership for GET, PATCH, and DELETE, while workspace-level assets remain accessible. Contract tests cover denied, authorized, and exempt access paths.

Changes

Workspace file asset project scoping

Layer / File(s) Summary
Project membership authorization guard
apps/api/plane/app/views/asset/v2.py
Adds ProjectMember validation through has_project_asset_access and returns 403 before mutations or signed URL generation when access is denied.
Contract coverage for scoped asset access
apps/api/plane/tests/contract/app/test_workspace_file_asset_project_scope_app.py
Adds fixtures and tests for non-member denial, active project-member access, and workspace-level asset exemption across GET, PATCH, and DELETE.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WorkspaceFileAssetEndpoint
  participant FileAsset
  participant ProjectMember

  Client->>WorkspaceFileAssetEndpoint: GET/PATCH/DELETE asset
  WorkspaceFileAssetEndpoint->>FileAsset: lookup workspace-scoped asset
  WorkspaceFileAssetEndpoint->>ProjectMember: check active membership for project-bound asset
  alt access denied
    WorkspaceFileAssetEndpoint-->>Client: 403 Forbidden
  else access granted or workspace-level asset
    WorkspaceFileAssetEndpoint-->>Client: continue operation or redirect
  end
Loading

Possibly related PRs

Suggested reviewers: pablohashescobar, dheeru0198, Saurabhkmr98

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: restricting workspace asset actions to project membership.
Description check ✅ Passed The description is detailed and covers summary, fix, tests, and references; only non-critical template sections are missing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch web-8066/workspace-asset-project-scope

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR closes an IDOR gap in WorkspaceFileAssetEndpoint by adding a project-membership authorization guard for workspace-scoped asset get/patch/delete operations when the asset is project-bound, and adds contract tests to prevent regressions tied to GHSA-h7mc-p9mm-2r4w / GHSA-cjph-cgm5-8pw8 (WEB-8066).

Changes:

  • Add a project_membership_denied() helper and invoke it in get/patch/delete to return 403 for non-project-members accessing project-bound assets.
  • Preserve workspace-level asset access (assets with project_id = NULL) for any active workspace member/guest.
  • Add contract tests covering denied access for non-project-members, plus positive/exemption controls.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
apps/api/plane/app/views/asset/v2.py Adds project-membership authorization guard to workspace-scoped asset fetch/mutation endpoints.
apps/api/plane/tests/contract/app/test_workspace_file_asset_project_scope_app.py Adds regression/contract coverage ensuring non-project-members receive 403 and no URL/mutations occur.

Comment thread apps/api/plane/app/views/asset/v2.py Outdated
…s workspace

Address Copilot review: filter ProjectMember by workspace_id=asset.workspace_id
in addition to project_id, mirroring allow_permission's PROJECT branch. Prevents
a member of the same project in a different workspace from passing the check if
an asset row is ever inconsistent (asset.workspace_id != project.workspace_id).

Co-authored-by: Plane AI <noreply@plane.so>
Comment thread apps/api/plane/app/views/asset/v2.py Outdated
else:
return

def project_membership_denied(self, request, asset):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should make this helper function return a boolean value and return Response in the view itself based on the value

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the latest commit — renamed to has_project_asset_access(request, asset) which now returns a boolean (True = allowed), and each of get/patch/delete builds the 403 Response itself based on the returned value. Behaviour is unchanged (same 403 + message; workspace-level assets with project_id=None still allowed). Existing 5 contract tests still pass.

…onse in views

Address review (Saurabhkmr98): rename project_membership_denied ->
has_project_asset_access, returning a boolean (True = allowed) instead of a
Response. Each of get/patch/delete now builds the 403 Response based on the
returned value. Behaviour is unchanged (same 403 + message; workspace-level
assets with project_id=None still allowed).

Co-authored-by: Plane AI <noreply@plane.so>
@dheeru0198
dheeru0198 merged commit e63f0c3 into preview Jul 14, 2026
14 checks passed
@dheeru0198
dheeru0198 deleted the web-8066/workspace-asset-project-scope branch July 14, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants