Skip to content

Any workspace member can publish (and delete) another user's private draft #9363

Description

@avisprince

Any workspace member can publish (and delete) another user's private draft

What we observed

Draft issues in Plane are personal — the list and detail endpoints filter by the current user, so each member only sees their own drafts. However, the "publish draft to issue" endpoint does not verify that the requesting user owns the draft. Any workspace member who knows (or guesses) the draft's UUID can convert it to a real issue and permanently delete the draft in the process.

1. Member publishes admin's private draft

User A (admin) created a draft issue with confidential planning notes. User B (member in the same workspace) called the draft-to-issue endpoint with the draft's ID. Despite never being able to see the draft through the listing or detail APIs, User B's request succeeded with 201. The draft was converted into a real issue in the project, and the original draft record was deleted. We verified at the database level that the draft no longer exists and the issue was created with the admin's original content.

Impact

  • Any workspace member can force-publish another user's unfinished draft, exposing work-in-progress content before the author intended
  • The draft is permanently deleted as part of the publish operation — the original author loses their draft without warning or any audit trail pointing to who published it
  • Since draft IDs are UUIDs, a determined attacker with workspace access could enumerate recent drafts via timing or ID pattern analysis

Steps to reproduce

Environment:

  • makeplane/plane-backend:stable
  • PostgreSQL 15.7, Valkey 7.2.11, RabbitMQ 3.13.6, MinIO
  • ENABLE_EMAIL_PASSWORD=1, ENABLE_SIGNUP=1

Setup:

  1. Complete instance admin sign-up (User A) and sign up a second user (User B)
  2. Create a workspace with User A
  3. Add User B to the workspace as a MEMBER (role 15)
  4. Create a project within the workspace

Authenticate both users (session cookies from sign-up/sign-in).

User A creates a draft:

POST /api/workspaces/{slug}/draft-issues/
Cookie: sessionid={admin_session}
Content-Type: application/json

{"name": "Admin's Confidential Draft", "description_html": "<p>This draft contains private planning notes</p>", "project_id": "{project_id}", "priority": "high"}

Returns 201 with the draft's id.

Control — User B cannot see User A's draft in listing:

GET /api/workspaces/{slug}/draft-issues/
Cookie: sessionid={member_session}

Returns 200 with an empty results array — User B has no drafts, and User A's draft is correctly hidden.

Bug — User B publishes User A's draft:

POST /api/workspaces/{slug}/draft-to-issue/{draft_id}/
Cookie: sessionid={member_session}
Content-Type: application/json

{"name": "Admin's Confidential Draft", "description_html": "<p>This draft contains private planning notes</p>", "priority": "high"}

Returns 201. The draft has been converted to a real issue in the project.

Database verification:

-- Draft was deleted
SELECT count(*) FROM draft_issues WHERE id = '{draft_id}' AND deleted_at IS NULL;
-- Returns 0

-- Issue was created with the original draft content
SELECT name, priority FROM issues WHERE id = '{issue_id}';
-- Returns name = 'Admin's Confidential Draft', priority = 'high'

Reproducing with Dokkimi

Dokkimi is an open-source testing tool that stands up isolated Docker environments from declarative YAML definitions — services, databases, seed data, test steps, and assertions all in one file. The definition for this bug is in dokkimi/dokkimi-in-the-wild/.dokkimi/planedokkimi run reproduces it from scratch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions