Skip to content

Cross-Project BOLA: Workspace Member Downloads Confidential Issue Attachments From Non-Member Projects (incomplete fix for GHSA-qw87-v5w3-6vxx)

Moderate
mguptahub published GHSA-h7mc-p9mm-2r4w Sep 28, 2026

Software

makeplane/plane

Affected versions

<= 1.3.1

Patched versions

1.4.0

Description

Duplicate of GHSA-85h2-mhcc-xfmw.

This report describes the same underlying bug: WorkspaceFileAssetEndpoint.get scopes file lookups to the workspace instead of the project, letting any workspace member read another project's files. The v1.4 test file cites this report together with GHSA-cjph-cgm5-8pw8; GHSA-85h2-mhcc-xfmw reported the same handler three weeks earlier. See GHSA-85h2-mhcc-xfmw for the authoritative report and remediation details. This advisory is being withdrawn to avoid duplicate tracking; the reporter's credit for this finding has been carried over to the surviving advisory.

Summary

The fix for GHSA-qw87-v5w3-6vxx (patched in v1.3.1) added @allow_permission(level="WORKSPACE") to WorkspaceFileAssetEndpoint, which blocks cross-workspace access. However, the fix is incomplete: the handler still does not check project membership for project-scoped assets (e.g., ISSUE_ATTACHMENT). Any workspace member — even a GUEST who belongs to a different project — can download confidential issue attachments from projects they are not a member of.

Details

Vulnerable code — apps/api/plane/app/views/asset/v2.py line 413:

@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug, asset_id):
    asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug)
    # ← NO project membership check
    # ← NO entity_type-based access control
    signed_url = storage.generate_presigned_url(...)
    return HttpResponseRedirect(signed_url)

The query filters only by workspace__slug — any workspace member passes. The project-scoped twin endpoint (ProjectFileAssetEndpoint) correctly adds project_id= to the filter, but this workspace-level endpoint does not.

Note: StaticFileAssetEndpoint explicitly refuses non-cover entity types (only allows USER_AVATAR, USER_COVER, WORKSPACE_LOGO, PROJECT_COVER), confirming issue attachments are NOT meant to be publicly accessible.

Endpoint: GET /api/assets/v2/workspaces/<slug>/<asset_id>/

PoC

Self-contained, idempotent, stdlib-only Python script:

BASE=http://localhost:8000 python3 poc_asset_bola.py

Requires two files: poc_asset_bola.py + plane_poc.py (shared library). Both attached as a comment.

Output from live run:

[base  ] owner GET asset -> HTTP 302
[ctrl  ] guest project-members/me on A's project -> HTTP 404 (outsider to the project)
[attack] guest GET A's issue attachment -> HTTP 302
[ctrl  ] non-workspace outsider GET asset -> HTTP 403 (denied as expected)
PASS — workspace guest read a confidential attachment from a project they are not in

Impact

  • Confidentiality (High): Any workspace member (even a GUEST in a different project) can read confidential attachments from projects they have no access to — internal documents, screenshots, sensitive files attached to private issues.
    • CVSS 3.1: 6.5 (Medium) — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
    • CWE-639 (Authorization Bypass Through User-Controlled Key)

Suggested Fix

Add project membership verification for project-scoped entity types:

if asset.entity_type in PROJECT_SCOPED_ENTITY_TYPES:
    if not ProjectMember.objects.filter(
        project_id=asset.project_id, member=request.user, is_active=True
    ).exists():
        return Response(status=status.HTTP_403_FORBIDDEN)

Severity

Moderate

CVE ID

No known CVE

Weaknesses

No CWEs

Credits