If you self-host Plane, removing someone from a project doesn’t keep them out. An access control bug, found by Maze Code and validated by our research team, gives a removed user their old access back, at their old role, as soon as they accept an invite to any other project in the same workspace.
If you haven’t used Plane, it’s an open-source alternative to Jira and Linear with over 60,000 GitHub stars, and many teams self-host it for project management.
Here’s how this vulnerability could play out. You remove a contractor who was an admin on a project that holds payroll data. Months later, a manager invites the same contractor to an unrelated design project. The contractor clicks accept and is added back to the payroll project as an admin. The manager who sent the invite sees “Project Invitation Accepted,” and the contractor never shows up in the design project.
How’s that happen? It comes down to how Plane handles removal, because removing someone from a project doesn’t delete anything. It flips a flag, and the old membership row stays in the database. Plane’s invite flow mistakenly finds that old row and switches it back on.
Maze Code found the bug by reading how Plane adds, removes and re-adds project members, and working through what happens to the old rows.
As with every vulnerability we find, we responsibly disclosed this one to Plane’s security team on April 21, 2026, and followed up three more times to help resolve it. As of this blog post, we haven’t heard back, but on September 22 Plane merged a fix that matches the one we recommended and shipped it in the v1.4.3-rc1 pre-release on October 6. The latest stable release, v1.4.2, is still vulnerable, and there’s currently no security advisory. We’re publishing the full details so self-hosted teams can check their data and patch before a stable release lands.
Are you affected?
Maze worked out what has to be true for an instance to be exposed, as it does for every vulnerability it investigates. You’re vulnerable if all three of these are true:
- You self-host Plane on v1.4.2 or earlier, or you run a build from the master branch.
- Someone in your workspace is, or used to be, a member of at least one project. Having been removed from that project still counts.
- That person accepts an invite to a different project in the same workspace.
The bug goes back a long way, and every release we checked, from v0.23.0 (October 2024) through v1.4.2, has the same vulnerable lookup. We confirmed it end-to-end on commit c62930ebcf in April and again on 1c8a60f858 in August.
The removed project doesn’t come back every time. Plane switches back on whichever of the user’s project memberships in the workspace was created most recently. If that’s a project they’re still on, their access doesn’t change. Either way, they never get added to the project they were invited to.
That missing membership is how you find past cases. Look for accepted invites where the user never became a member of the invited project.
SELECT i.project_id, i.email, i.responded_at
FROM project_member_invites i
JOIN users u ON lower(u.email) = lower(i.email)
LEFT JOIN project_members m
ON m.project_id = i.project_id AND m.member_id = u.id
WHERE i.accepted AND m.id IS NULL;
For each finding, review that user’s other memberships in the workspace and their roles.
How to fix it and clean up
Upgrade to v1.4.3-rc1, run the preview branch, or apply the patch from PR #9866 (commit bae037cfb2). As of October 19, 2026, no stable release includes it. The patch scopes the lookup to the invited project and applies the invite’s role when someone rejoins.
project_member = ProjectMember.objects.filter(
workspace_id=project_invite.workspace_id,
project_id=project_id,
member=user,
).first()
# ...
project_member.role = project_invite.role
After you patch, run the query above and remove any access that came back by mistake. Earlier acceptances already changed the data, and the patch doesn’t undo them.
How it works
Plane organizes work into workspaces, and each workspace holds many projects. A user’s access to a project lives in a ProjectMember row with a role and an is_active flag. Roles run from guest (5) to admin (20).
Removing a user sets is_active to False and keeps the row. That makes rejoining cheap, since the join code can find the old row and switch it back on.
The vulnerable path: a lookup that never checks the project
When a user accepts a project invite, ProjectJoinEndpoint.post in apps/api/plane/app/views/project/invite.py runs this.
project_member = ProjectMember.objects.filter(
workspace_id=project_invite.workspace_id, member=user
).first()
if project_member is None:
ProjectMember.objects.create(
project_id=project_id, member=user, role=project_invite.role
)
else:
project_member.is_active = True
project_member.role = project_member.role
project_member.save()
The filter checks the workspace and the user, but it never checks the project. If the user has any membership row in the workspace, .first() returns their newest one, and the else branch switches it back on. Often that’s the project they were removed from.
Why the old role survives
The role line assigns the row’s role back to itself, when it was almost certainly meant to read project_invite.role. As written, the invite’s role is never applied, so a former admin invited somewhere else as a guest comes back as an admin on the old project.
What the admin sees
The endpoint returns a 200 with “Project Invitation Accepted,” even though nobody was added to the invited project. Because the invite is marked as accepted, sending it again doesn’t repair anything, and every new invite repeats the same wrong reactivation.
The logs don’t help much either, because the invite record names one project and the only membership change happened on another.
We rate it High severity, with a CVSS 3.1 score of 8.1, because anyone with a regular Plane account can exploit it over the network without any special setup, and it gives them read and write access to a project they were removed from (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N). The underlying weakness is CWE-863, incorrect authorization.
Proof of concept
Our proof of concept is an automated test, written in pytest, that runs against Plane’s unmodified development setup. In it, a user who was an admin on project X is removed and then accepts a guest invite to project Y.
membership_x = ProjectMember.objects.create(
workspace=workspace, project=project_x, member=victim, role=20, is_active=False
)
invite_y = ProjectMemberInvite.objects.create(
project=project_y, workspace=workspace, email=victim.email,
token="invite-token-y", role=5, created_by=create_user,
)
response = client.post(
f"/api/workspaces/{workspace.slug}/projects/{project_y.id}/join/{invite_y.pk}/",
{"token": "invite-token-y", "accepted": True},
format="json",
)
assert response.status_code == 200
membership_x.refresh_from_db()
assert membership_x.is_active is True # removed project X is back, still admin
assert not ProjectMember.objects.filter(project=project_y, member=victim).exists() # Y got nothing
The test passes on unpatched code and fails once the fix is applied, so it also works as a regression test.
Why previous scanners missed this vulnerability
We run Maze Code against open-source projects that our customers and the wider community rely on, so we can find new vulnerabilities, report them to maintainers and get them fixed before attackers find them. In Plane, that review led Maze Code to work through how project membership changes over time, from being added to being removed to rejoining.
No single line in the join code looks wrong. Reactivating an old member when they rejoin is a real feature, and one you’d expect from a platform like Plane, the query is valid Django, and no untrusted input reaches a dangerous call. A scanner looking for bad patterns has nothing to flag here, because the problem is which row the code picks.
Finding this bug means holding several facts in your head at once. Removing someone in Plane leaves their membership row behind, one user can have rows in many projects across a workspace, and the endpoint is meant to act on the project in its URL while the lookup ignores that project. Maze Code connected those facts and asked which row comes back for a user who has history in the workspace.
It also noticed the same file already does this lookup correctly. About 70 lines up, UserProjectInvitationsViewset.create filters by project ID. That contrast marked the missing filter as an oversight, and it showed what the fix should look like.
From there, Maze Code traced the outcome, working out which row comes back, what role it keeps and why the invited project ends up empty, and then wrote the test above to prove it.
Maze Code doesn’t find vulnerabilities by matching rules. We give the AI detailed instructions and guidance, so it can read code the way a security researcher would, working out what each part of the code is meant to guarantee and checking whether it does. In Plane, that guarantee was that removing someone from a project keeps them out.
Disclosure timeline
- On April 21, 2026, we reported the bug to security@plane.so with a proof of concept.
- On May 5, 2026, we followed up and didn’t hear back.
- On August 11, 2026, we sent an updated proof of concept, confirmed the bug was still present on commit 1c8a60f858, and noted that the 90-day disclosure window had passed.
- On August 31, 2026, we let Plane know we planned to publish now that we were past the disclosure window, and offered to work with them on a fix. We’ve yet to get a reply.
- On September 22, 2026, Plane merged a fix to its preview branch in PR #9866.
- On October 6, 2026, Plane shipped the fix in the v1.4.3-rc1 pre-release.
- On October 20, 2026, with no stable release or security advisory covering the bug, we published this post.
We’ve asked MITRE for a CVE and will add the ID here once it’s assigned, along with the stable release that ships the fix. Thanks to the Plane maintainers for merging it.
About Maze Code
Maze investigates every vulnerability the way your best security engineer would if they had all the time in the world, cutting through the noise to surface what’s real. It covers your dependencies, code and cloud, so you can find and fix what matters.
Maze Code has found exploitable CVEs like this one in several open-source projects that other AI AppSec tools had already looked at. Learn more about Maze Code.




