DEV Community

Rohit Sriram
Rohit Sriram

Posted on

I scanned Plane (50k stars) for bugs. Found 32. Zero false positives.

Plane is one of the most popular open source project management tools out there. Over 50,000 GitHub stars, actively maintained, used by real teams in production.

I scanned it with Faultmark, a code scanner I built that uses a multi-model AI debate to verify findings before surfacing them. Every candidate bug gets challenged by multiple models before it makes the final list. The goal is zero false positives.

The result: 32 confirmed bugs, 0 false positives.

Some of them are bad. Here are the ones that stood out.

GitHub and GitLab OAuth are both completely broken

In the space app, a local variable named base_host shadows the imported base_host function on line 61 of github.py. From that point forward, every call to base_host(request=request, is_space=True) fails with TypeError: 'str' object is not callable. The same shadowing pattern exists in the GitLab callback file. Anyone trying to log in via GitHub or GitLab on the space app gets a 500 error and never gets authenticated.

Existing users are treated as new signups, and new users are treated as returning logins

In adapter/base.py line 299, is_signup = bool(user) has the logic completely inverted. If the user already exists in the database, bool(user) is True, so is_signup is True. If the user is new and doesn't exist yet, is_signup is False. The onboarding flow triggers for returning users and gets skipped for new registrations.

Project invitations are fully broken in two separate ways

Bug 1: In invite.py line 105, project_invitations gets reassigned to the list returned by bulk_create(), then .delay() is called on that list. Lists do not have a .delay() method. Every invitation crashes with AttributeError: 'list' object has no attribute 'delay'.

Bug 2: In the same file at lines 60-64, .role is called directly on a QuerySet returned by .filter() instead of calling .first() first. AttributeError: 'QuerySet' object has no attribute 'role' on every invitation creation attempt.

Any workspace member can modify another member's preferences

In workspace/user_preference.py line 64, the query that fetches the preference to update does not filter by request.user. Any authenticated workspace member can send a PATCH request with another user's preference key and modify that user's pinned items and sort order. The fix is adding user=request.user to the filter.

LLM output is passed to the frontend without HTML escaping

In external/base.py, the response from the AI assistant goes through text.replace('\n', '
') without first escaping HTML entities. The resulting response_html field contains raw HTML. If rendered as innerHTML on the frontend, any LLM output containing script tags becomes stored XSS.

Mention notifications go to the wrong user

In notification_task.py line 576, EmailNotificationLog is created with receiver_id=subscriber instead of receiver_id=mention_id. The subscriber variable is the last value from an outer loop. The mentioned user never receives the email, and the log is recorded against the wrong person.

Updating a saved view silently wipes all its filters

In view.py, IssueViewSerializer.update() unconditionally overwrites the query field regardless of whether filters were included in the PATCH request. Renaming a view by sending {"name": "New Name"} resets all its saved filters to empty with no error and no warning.

Deleting an estimate point bulk-updates all issues on the first loop iteration

In estimate/base.py, issues.update(...) is called inside a for issue in issues: loop. issues.update() operates on the full queryset. On the first iteration, every issue gets updated. The loop then runs N-1 more times redundantly.

Those are the highlights. The other 24 confirmed bugs cover nil dereferences across cycle archiving, issue relation deletion, intake issue handling, search filtering, page access, and asset deletion. The full list is in the GitHub issue filed on the Plane repo.

Every finding was verified against the actual source files before being reported. The scan took about 10 minutes.

Faultmark is live at faultmark.com. Free tier includes 3 scans/month. If you want to see what it finds in your own codebase, try it.

Top comments (0)