* fix(auth): restore activation flow and narrow deactivation guard (GHSA-rmmf-rj2q-3rrg)
PR #9290 introduced two regressions in adapter/base.py:
1. is_signup = bool(user) was inverted — True when the user EXISTS means
the IDP sync ran on signup instead of login, and the callback received
the wrong value. Fixed to is_signup = not bool(user) matching EE.
2. The deactivation check blocked ALL inactive users, including accounts
provisioned with is_active=False that have never completed a first
login. Fixed by adding `and user.last_login_time is not None` — only
accounts that have previously logged in (and were then explicitly
deactivated by an admin) are rejected. Provisioned/never-logged-in
accounts still pass through to save_user_data().
3. Restore is_active=True and user_activation_email in save_user_data()
so provisioned accounts are properly activated on first login.
Co-authored-by: Plane AI <noreply@plane.so>
* fix(auth): save before email, use last_logout_time as deactivation discriminator
Two CR fixes on PR #9304:
1. save_user_data(): capture was_inactive flag, save() first, then send
activation email as a best-effort side-effect so a failed enqueue
cannot abort account activation.
2. complete_login_or_signup(): switch deactivation discriminator from
last_login_time to last_logout_time. The deactivation endpoint always
sets last_logout_time, making it a direct signal of explicit
deactivation. A provisioned account that was never deactivated has
last_logout_time=None and is correctly allowed through for first login,
even if it also has no last_login_time.
Co-authored-by: Plane AI <noreply@plane.so>
---------
Co-authored-by: Plane AI <noreply@plane.so>
GHSA-rmmf-rj2q-3rrg: save_user_data() was unconditionally setting
is_active=True on every login, silently reactivating any admin-deactivated
account. Fix: add an early guard in complete_login_or_signup() that raises
USER_ACCOUNT_DEACTIVATED (5019) before any session or save logic if the
existing user's is_active=False. Remove the is_active=True assignment and
the associated user_activation_email call from save_user_data(). Also
remove the now-unused user_activation_email and base_host imports.
GHSA-wjgv-cq7w-258v: WorkspaceOwnerPermission in both app/permissions/
and utils/permissions/ was filtering WorkspaceMember without is_active=True,
allowing a deactivated workspace owner/admin to retain API access. Add
is_active=True to both copies to match every other permission class.
Co-authored-by: Plane AI <noreply@plane.so>
* fix: prevent ORM order_by injection via user-supplied query params (GHSA-2r95, GHSA-w45q)
Add field-name allowlists and a sanitize_order_by() utility in order_queryset.py.
All allowlists are centralised there; each call site imports the named constant
so there are no inline sets scattered across view files.
- order_queryset.py: ISSUE_ORDER_BY_ALLOWLIST, INTAKE_ISSUE_ORDER_BY_ALLOWLIST,
ACTIVITY_ORDER_BY_ALLOWLIST, PROJECT_ORDER_BY_ALLOWLIST, VIEW_ORDER_BY_ALLOWLIST,
NOTIFICATION_ORDER_BY_ALLOWLIST + sanitize_order_by() utility; validation added
at the top of order_issue_queryset() — fixes all callers including the
unauthenticated ProjectIssuesPublicEndpoint (GHSA-w45q)
- api/views/cycle.py, api/views/module.py: cycle/module issue list endpoints
- api/views/issue.py: IssueActivity list and detail endpoints
- app/views/intake/base.py: IntakeIssue list
- app/views/view/base.py: saved-view list
- app/views/notification/base.py: notification paginator
- app/views/project/base.py: project list paginator
- app/views/user/base.py, app/views/workspace/user.py: activity paginators
Closes WEB-7813
Co-authored-by: Plane AI <noreply@plane.so>
* fix: harden sanitize_order_by against multi-dash malformed inputs
lstrip("-") stripped all leading dashes, allowing "--created_at" to
pass the allowlist check unchanged and reach .order_by() as a malformed
token (causing FieldError). Now strips only one leading dash; any
remaining dash prefix is rejected to the safe default.
Co-authored-by: Plane AI <noreply@plane.so>
---------
Co-authored-by: Plane AI <noreply@plane.so>
* fix: remove hardcoded SECRET_KEY from community deployment manifests (GHSA-cmwv-pjmw-8483)
Replace the publicly-known default SECRET_KEY and LIVE_SERVER_SECRET_KEY values
in AIO and CLI community deployment manifests with a safe placeholder.
- deployments/aio: variables.env now ships with placeholder values;
start.sh auto-generates a random key on first boot (or on upgrade from the old
insecure default) and persists it in plane.env across restarts
- deployments/cli: variables.env ships with placeholder; docker-compose.yml
fallbacks that referenced the publicly-known default are removed
- apps/api/plane/settings/common.py: SECRET_KEY resolution now uses `or`
so an empty env var falls back to get_random_secret_key() (not ""); adds a
startup warning if the known insecure default or placeholder is detected
Closes WEB-7805
Co-authored-by: Plane AI <noreply@plane.so>
* fix: use logger.critical instead of print for insecure SECRET_KEY warning
Address code review feedback — replace module-level print() with _logger.critical()
and move _logger definition before the SECRET_KEY block to avoid duplicate assignment.
Also removes the now-unused `import sys`.
Co-authored-by: Plane AI <noreply@plane.so>
---------
Co-authored-by: Plane AI <noreply@plane.so>
GHSA-933r-rxg8-f3h2 — EstimatePointEndpoint.create trusted the
estimate_id URL parameter without verifying it belonged to the caller's
workspace and project. An authenticated user in project A could inject
estimate points into any other workspace's estimate by supplying a
foreign estimate_id.
Fix: added a workspace+project scoped Estimate ownership check before
EstimatePoint.objects.create().
GHSA-933r-rxg8-f3h2 (destroy) — old_estimate_point was fetched with
pk only (unscoped), allowing cross-tenant key disclosure and
manipulation during the key-rearrangement step.
Fix: scoped the old_estimate_point lookup to estimate_id + project_id +
workspace__slug; added 404 guard for missing/foreign points.
Note: BulkEstimatePointEndpoint.partial_update (GHSA-vm3j-5j49-gwrf)
was already correctly scoped at lines 116 and 125-130 — no change needed.
Co-authored-by: Plane AI <noreply@plane.so>
GHSA-6qrq-f73q-r67j / GHSA-j9pv-f5wm-p4g2 — IssueCommentSerializer in
both app and api layers stored comment_html without sanitization. The app
layer had no validate() at all; the api layer only ran lxml structural
normalization which does not strip XSS payloads.
Fix: both serializers now call validate_html_content() (nh3-backed) in
their validate() methods, replacing the raw value with sanitized HTML.
GHSA-hh2r-3hwp-mvq3 — space/views/intake.py and api/views/intake.py
both used bare Issue.objects.create() with description_html taken
directly from request data, bypassing any serializer validation.
Fix: both paths now call validate_html_content() and pass the sanitized
value to Issue.objects.create(). Falls back to "<p></p>" if sanitizer
returns None (empty/invalid input).
The nh3 sanitizer (validate_html_content in content_validator.py) was
already present and used by IssueCreateSerializer — this change extends
coverage to the two remaining unsanitized comment and intake paths.
Co-authored-by: Plane AI <noreply@plane.so>
- Add WorkSpaceMemberInvitePublicSerializer that excludes token and
invite_link; use it in WorkspaceJoinEndpoint.get() so an unauthenticated
caller cannot retrieve the acceptance token from the GET endpoint
(GHSA-86mg-259g-pwgg / GHSA-gf48-p6jp-cwc4).
- Require authentication and verify request.user.email matches the
invited email before accepting a workspace invitation so an attacker
who registers with the invited address cannot hijack the invite
(GHSA-4vj8-p63v-8p24).
Co-authored-by: Plane AI <noreply@plane.so>
* fix: scope workspace user preference filter to current user
Without user=request.user on the PATCH filter, the ORM could match
another user's preference record in the same workspace, causing
pin/unpin state to leak across users or silently fail to persist.
Fixes#9260
Signed-off-by: okxint <cashmein.eth@gmail.com>
* test: add regression coverage for workspace user preference scoping (#9260)
Adds contract tests for the sidebar preference PATCH endpoint:
- test_patch_only_updates_requesting_users_preference: in a multi-member
workspace, a member's PATCH must update only their own preference row,
never another member's. Fails against the pre-fix code (the unscoped
.first() mutates the most-recently-created row regardless of user).
- test_patch_updates_own_preference: baseline that a member's PATCH
persists to their own row.
Verified RED on the unpatched view and GREEN with the user=request.user
filter from #9261.
* fix(api): wrap long line to satisfy ruff E501 in user preference view
---------
Signed-off-by: okxint <cashmein.eth@gmail.com>
Co-authored-by: okxint <cashmein.eth@gmail.com>
* fix: Use APP_DOMAIN env var for bot user email instead of hardcoded plane.so
Signed-off-by: okxint <cashmein.eth@gmail.com>
* use settings.WEB_URL instead of APP_DOMAIN env var for bot email domain
---------
Signed-off-by: okxint <cashmein.eth@gmail.com>
* fix(api): require at least one alphanumeric char in workspace name
Workspace name validation was enforced only on the frontend
(validateWorkspaceName), which gates the UI submit but is bypassable
via a direct API call. The backend WorkSpaceSerializer.validate_name
only rejected URLs, so a symbol-only name like "-_________-" could
still be saved via create or the rename (partial_update) path.
Add a Unicode-aware has_alphanumeric() helper and enforce it in both
the app and instance/license workspace serializers, mirroring the
frontend HAS_ALPHANUMERIC_REGEX (/[\p{L}\p{N}]/u) added in #9263.
International names (日本語, José, محمد) still pass since str.isalnum()
covers all scripts.
Adds unit tests covering symbol-only rejection and international
acceptance on both serializers.
Refs #9255
Signed-off-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
* fix(api): reject URLs in instance workspace name for parity
Address CodeRabbit review on #9278: the instance/license
WorkspaceSerializer.validate_name rejected symbol-only names but, unlike
the app-level WorkSpaceSerializer, still accepted names containing URLs.
Add the same contains_url() guard (imported from plane.utils.url, not
content_validator) so both workspace-create paths validate identically.
Add unit tests asserting URL-containing names are rejected on both
serializers.
Signed-off-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
---------
Signed-off-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
* fix(security): scope issue ID validation to workspace/project in bulk endpoints
Prevents cross-tenant IDOR by filtering incoming issue IDs through
workspace+project scope before bulk_create/bulk_update in:
- CycleIssueListCreateAPIEndpoint: validate new_issues against workspace+project (GHSA-22g9-9xfv-q3fr)
- SubIssuesEndpoint: validate sub_issue_ids against workspace (GHSA-38vj-gf85-7q5x)
- IssueRelationListCreateAPIEndpoint: validate issues against workspace (GHSA-8cvv-8jh5-g6mj)
- ModuleIssueListCreateAPIEndpoint: already scoped at line 673, no change needed (GHSA-x5c5-hmvm-94v9)
Co-authored-by: Plane AI <noreply@plane.so>
* fix(security): extend IDOR scope validation to app-layer endpoints
Same cross-tenant IDOR fix applied to the app/views/ counterparts
which are used by the web frontend (api/views/ covered in previous commit):
- app/views/cycle/issue.py: filter new_issues to workspace+project (GHSA-22g9-9xfv-q3fr)
- app/views/module/issue.py: filter issues to workspace+project before bulk_create (GHSA-x5c5-hmvm-94v9)
- app/views/issue/relation.py: filter issues to workspace before bulk_create (GHSA-8cvv-8jh5-g6mj)
Co-authored-by: Plane AI <noreply@plane.so>
* chore: remove advisory ID references from code comments
---------
Co-authored-by: Plane AI <noreply@plane.so>
Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
`COMPANY_NAME_REGEX` blocks disallowed chars but accepts symbol-only
strings like `-_________-` since `-` and `_` are in the allowed set.
Add `HAS_ALPHANUMERIC_REGEX` and check it in `validateWorkspaceName`
and `validateCompanyName` so inputs with no letter or digit are rejected.
Fixesmakeplane/plane#9255
Signed-off-by: okxint <cashmein.eth@gmail.com>
* fix(security): scope cascade deletes to workspace in BulkDeleteIssuesEndpoint
CycleIssue and ModuleIssue cascade deletes used raw issue_ids from the
request instead of the already workspace+project scoped issues queryset,
allowing cross-workspace deletion of related records.
Fixes GHSA-6cw7-h92q-p9hg and GHSA-2rr4-rp7r-32p4.
GHSA-7q7r-mrr4-2wwx (sub-issue parent reassign) covered in WEB-7727.
Co-authored-by: Plane AI <noreply@plane.so>
* chore: remove advisory ID reference from code comment
---------
Co-authored-by: Plane AI <noreply@plane.so>
Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>