makeplane / makeplane/plane

cross-tenant comment-injection IDOR in the public deploy-board comment endpoint

Open
#9,441 2 comments 0 reactions 2 assignees View on GitHub

@vihar is already working on this.

Since Jul 19, 2026.

🐛bug plane
Dominant language
TypeScript
Stars
59.6k
Forks
5.8k
Avg merge
1d 22h
Merged PRs (30d)
49

Description

Is there an existing issue for this?
  • I have searched the existing issues
Current behavior

reported via email on 13 June 2026 - no response.
can I share details here?

Summary

The public deploy-board comment-create endpoint resolves the board's project from the anchor, but writes the comment using the issue_id path parameter verbatim with no check that the issue belongs to that board's project or workspace. Any authenticated user who controls one published board with comments enabled can write attacker-controlled comment HTML onto any issue in the entire instance, including private, never-published projects in other workspaces. Confirmed against the shipped view: an attacker who is a member of only workspace A wrote a comment onto workspace B's private issue.

Details

Sink: apps/api/plane/space/views/issue.py, IssueCommentPublicViewSet.create() (~257-294):

serializer.save(project_id=project_deploy_board.project_id, issue_id=issue_id, actor=request.user, access="EXTERNAL")

Route: POST /api/public/anchor/<anchor>/issues/<issue_id>/comments/ (apps/api/plane/space/urls/issue.py). The anchor resolves a published DeployBoard to get project_id; the issue_id path param is taken verbatim and written as the comment's issue_id. The view validates only that a board exists for the anchor and that is_comments_enabled is true; it never verifies that issue_id belongs to that board's project/workspace. issue and project are read_only on IssueCommentSerializer, so they come only from these save() kwargs.

The boundary crossed is cross-tenant/cross-workspace. The create action requires IsAuthenticated (a self-registered account on a multi-tenant instance suffices); an attacker who owns one published, comments-enabled board can write comment HTML onto any issue instance-wide. The resulting comment carries the attacker's project_id but the victim's issue_id (data corruption) and fires issue_activity.delay(...) against the victim issue (activity-log/notification pollution). This is the same class as the patched CVE-2026-39374 (bulk date IDOR) and CVE-2026-27705 (asset patch IDOR), but on a distinct, unreported endpoint; none of the 10 published advisories cover deploy-board/space comment endpoints.

PoC

scripts/poc_plane_deploy_board_comment_idor.md.

POST /api/public/anchor/<attacker board anchor>/issues/<VICTIM private issue id>/comments/
Authorization: <attacker session, member of attacker workspace only>
{ "comment_html": "<p>INJECTED-BY-ATTACKER</p>" }
-> 201; a comment is written onto the victim's private issue

Validated by booting Postgres 15 + Redis 7, creating a venv with the repo's pinned deps, and running a pytest inside the repo's own harness (plane.settings.test conftest/factories) that drives the unmodified shipped view through Django's URL resolver + DRF APIClient. Workspace A (attacker, member of A only) has a published board with comments enabled; Workspace B (separate owner, no shared membership) has a private, never-published project and a confidential issue. Authenticated as the attacker, POSTed to A's anchor with B's private issue_id:

STATUS: 201
WRITTEN COMMENT id=5efd10b6... actor=f48b0ffa-(attacker, WS-A only)
   issue=8b126c57-(B's confidential issue) project=626c3d95-(A's project)
   html='<p>INJECTED-BY-ATTACKER pwn</p>'
CALL kwargs: 8b126c57(victim issue) 626c3d95(attacker project)

Asserts confirmed the attacker is not a WorkspaceMember of B, B's project has no DeployBoard, and the issue had 0 comments before, yet exactly one attacker-authored comment exists on B's private issue afterward. The only stub was the issue_activity Celery task dispatched after the unauthorized DB write (peripheral, no auth bearing); git status confirmed the view is byte-for-byte the shipped code.

Impact

Any low-privilege authenticated user who owns one comments-enabled published board writes attacker-controlled comments onto arbitrary issues across all workspaces/tenants, including private projects they cannot access, corrupting the issue/project linkage and polluting activity logs/notifications. Cross-tenant integrity breach; scope changed.

Remediation

In create (and consistently in partial_update, destroy, and the reaction/vote create handlers and the comment/reaction get_queryset filters, which similarly scope only to workspace_id), verify the target object belongs to the board's project before writing:

if not Issue.objects.filter(id=issue_id, project_id=project_deploy_board.project_id).exists():
    return Response({"error": "Issue not found"}, status=404)

Apply the same project_id-scoping to IssueVotePublicViewSet.create/destroy (unvalidated issue_id) and CommentReactionPublicViewSet (unvalidated comment_id).

Version

commit fd16d03

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.