fix(api): require token to view workspace invitation (GHSA-gf48-p6jp-cwc4)#9015
fix(api): require token to view workspace invitation (GHSA-gf48-p6jp-cwc4)#9015sriramveeraghanta wants to merge 1 commit intopreviewfrom
Conversation
…cwc4) The unauthenticated GET /api/workspaces/<slug>/invitations/<pk>/join/ endpoint serialized the invitation with `fields = "__all__"`, exposing the secret invitation `token` (and an `invite_link` containing it) to any caller who could guess a workspace slug + invitation id. Reject GET requests that don't supply the matching `?token=` from the emailed invite link, comparing in constant time, and additionally strip `token` and `invite_link` from the response as defense in depth. Frontend callers (`/workspace-invitations` page, auth header) now thread the token from the URL through `getWorkspaceInvitation`. Auth pages visited without an invite token simply skip the invitation banner SWR call — they have no proof of access and shouldn't see invitation data.
There was a problem hiding this comment.
Pull request overview
This PR addresses GHSA-gf48-p6jp-cwc4 by preventing unauthenticated callers from retrieving workspace invitation secrets and metadata via the workspace invitation “join” GET endpoint, and updates the web client to supply the invitation token where needed.
Changes:
- Backend: require
?token=forGET /api/workspaces/<slug>/invitations/<pk>/join/, compare in constant time, and return uniform403responses; removetokenandinvite_linkfrom the successful response payload. - Frontend: update invitation fetch flows to require/forward the token (workspace invitation page + auth header invitation context).
- Minor frontend lint-related refactors in touched components.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/web/core/services/workspace.service.ts | Requires a token when fetching workspace invitation details and forwards it as a query param. |
| apps/web/core/components/account/auth-forms/auth-root.tsx | Reads token from URL params and passes it into AuthHeader for invite-context rendering. |
| apps/web/core/components/account/auth-forms/auth-header.tsx | Gates SWR invitation fetch on presence of invitationToken and uses the token in the fetcher call. |
| apps/web/app/(all)/workspace-invitations/page.tsx | Requires token to fetch invitation details and keeps accept/reject flows working with token present. |
| apps/api/plane/app/views/workspace/invite.py | Enforces token validation for unauthenticated invitation detail retrieval and strips secret fields from the response. |
| const { data: invitation, isLoading } = useSWR( | ||
| workspaceSlug && invitationId ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null, | ||
| async () => workspaceSlug && invitationId && workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId), | ||
| workspaceSlug && invitationId && invitationToken ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null, | ||
| async () => | ||
| workspaceSlug && | ||
| invitationId && | ||
| invitationToken && | ||
| workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId, invitationToken), |
| invitation_id && slug && token && WORKSPACE_INVITATION(invitation_id.toString()), | ||
| invitation_id && slug && token |
📝 WalkthroughWalkthroughA security enhancement to workspace invitations that adds token-based validation. The backend now requires a ChangesWorkspace Invitation Token Validation
Sequence DiagramsequenceDiagram
participant Page as Workspace Invitations Page
participant AuthRoot as AuthRoot Component
participant AuthHeader as AuthHeader Component
participant Service as WorkspaceService
participant API as Backend API Endpoint
Page->>AuthRoot: Extract token from URL query params
AuthRoot->>AuthHeader: Pass invitationToken prop
AuthHeader->>AuthHeader: Build SWR key with token
AuthHeader->>Service: getWorkspaceInvitation(slug, id, token)
Service->>API: GET /invite with token query param
API->>API: Fetch invitation record
API->>API: Constant-time compare provided vs stored token
alt Token Valid
API->>API: Remove token & invite_link from response
API->>Service: Return filtered invitation data
Service->>AuthHeader: Invitation details
AuthHeader->>Page: Render invitation UI
else Token Invalid/Missing
API->>Service: Return 403 Forbidden
Service->>AuthHeader: Error state
AuthHeader->>Page: Deny access
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/app/`(all)/workspace-invitations/page.tsx:
- Around line 41-45: The SWR cache key currently only includes invitation_id
(via WORKSPACE_INVITATION) but not token, so changing token won't cause a
refetch; update the useSWR key to include token (and slug if desired) so each
distinct token has its own cache entry — e.g., when calling useSWR ensure the
first arg contains token (for example an array or a key string that includes
token) and keep the same fetcher
workspaceService.getWorkspaceInvitation(slug.toString(),
invitation_id.toString(), token.toString()) so SWR will create separate entries
per token.
In `@apps/web/core/components/account/auth-forms/auth-header.tsx`:
- Around line 66-72: The SWR cache key used in useSWR (currently
`WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}`) omits invitationToken,
allowing stale/unauthorized responses to be reused; update the key to include
invitationToken (e.g.,
`WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}_${invitationToken}`) and
ensure the same workspaceSlug && invitationId && invitationToken condition
guards the hook so the cache differentiates requests by token for
workSpaceService.getWorkspaceInvitation.
In `@apps/web/core/services/workspace.service.ts`:
- Around line 172-180: getWorkspaceInvitation currently declares it returns
IWorkspaceMemberInvitation but the API response omits token and invite_link;
change the method's return type to reflect that by using Omit to remove those
keys (e.g. Promise<Omit<IWorkspaceMemberInvitation, 'token' | 'invite_link'>>),
update any related imports/signatures if needed, and ensure callers are adjusted
to the narrower shape so TypeScript strict mode is satisfied; keep the
getWorkspaceInvitation implementation unchanged aside from the updated return
type.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 34cee557-ff0c-4259-86c3-fcbddc0349f1
📒 Files selected for processing (5)
apps/api/plane/app/views/workspace/invite.pyapps/web/app/(all)/workspace-invitations/page.tsxapps/web/core/components/account/auth-forms/auth-header.tsxapps/web/core/components/account/auth-forms/auth-root.tsxapps/web/core/services/workspace.service.ts
| const { data: invitationDetail, error } = useSWR( | ||
| invitation_id && slug && WORKSPACE_INVITATION(invitation_id.toString()), | ||
| invitation_id && slug | ||
| ? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString()) | ||
| invitation_id && slug && token && WORKSPACE_INVITATION(invitation_id.toString()), | ||
| invitation_id && slug && token | ||
| ? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString(), token.toString()) | ||
| : null |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check if the file exists and read the relevant section
cat -n apps/web/app/\(all\)/workspace-invitations/page.tsx | sed -n '35,55p'Repository: makeplane/plane
Length of output: 956
🏁 Script executed:
# Also check the structure of WORKSPACE_INVITATION function to understand what it returns
grep -n "WORKSPACE_INVITATION" apps/web/app/\(all\)/workspace-invitations/page.tsx -B 2 -A 2Repository: makeplane/plane
Length of output: 604
🏁 Script executed:
# Search for WORKSPACE_INVITATION definition across the codebase
rg "WORKSPACE_INVITATION\s*=" --type ts --type tsx -A 3Repository: makeplane/plane
Length of output: 87
🏁 Script executed:
# Search for WORKSPACE_INVITATION definition
rg "WORKSPACE_INVITATION" -A 3 | head -50Repository: makeplane/plane
Length of output: 3581
🏁 Script executed:
# Check what the function returns
fd "fetch-keys" -type f | head -5Repository: makeplane/plane
Length of output: 230
🏁 Script executed:
# Let's read the fetch-keys file to see WORKSPACE_INVITATION definition
find . -name "*fetch-keys*" -type fRepository: makeplane/plane
Length of output: 99
🏁 Script executed:
# Let's check the fetch-keys file to see the full context
cat -n apps/web/core/constants/fetch-keys.ts | head -50Repository: makeplane/plane
Length of output: 2168
🏁 Script executed:
# Check if there are other usages of SWR with token in cache keys
rg "useSWR.*token" -B 2 -A 3 | head -100Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# Look at the auth-header.tsx example more closely
cat -n apps/web/core/components/account/auth-forms/auth-header.tsx | grep -A 10 -B 5 "WORKSPACE_INVITATION"Repository: makeplane/plane
Length of output: 765
🏁 Script executed:
# Check if there are other invitation-related SWR calls with token parameters
rg "getWorkspaceInvitation|joinWorkspace" -B 5 -A 5 | head -80Repository: makeplane/plane
Length of output: 5954
🏁 Script executed:
# Let's check if invitation_id is globally unique or workspace-specific by looking at the API
rg "invitations.*invitation_id" --type ts -B 5 | head -60Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# Check SWR caching behavior and if there are comments about token-based caching
cat -n apps/web/app/\(all\)/workspace-invitations/page.tsx | sed -n '1,80p'Repository: makeplane/plane
Length of output: 3194
🏁 Script executed:
# Let's also verify the WORKSPACE_INVITATION constant more thoroughly
rg "export const WORKSPACE_INVITATION" -A 2Repository: makeplane/plane
Length of output: 350
🏁 Script executed:
# Check the workspace service getWorkspaceInvitation method signature
cat -n apps/web/core/services/workspace.service.ts | grep -A 15 "async getWorkspaceInvitation"Repository: makeplane/plane
Length of output: 615
🏁 Script executed:
# Look at API endpoint to understand if token is user-specific or invitation-specific
rg "invitations.*join" -B 3 -A 8 | head -40Repository: makeplane/plane
Length of output: 2927
🏁 Script executed:
# Check if there are other places in the codebase where token is included in SWR keys
rg "useSWR.*token.*\[" -B 1 -A 5 | head -60Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# Check if there are any comments or documentation about SWR key design in the project
rg "SWR.*key|cache.*key" --type ts -B 2 -A 2 | head -50Repository: makeplane/plane
Length of output: 1143
🏁 Script executed:
# Let's verify the exact behavior difference - check if slug is used in other similar patterns
rg "WORKSPACE_INVITATION\(" -B 3 -A 3Repository: makeplane/plane
Length of output: 754
🏁 Script executed:
# Verify the diff format is correct for SWR array keys
rg "useSWR.*\[" -B 1 -A 3 | head -50Repository: makeplane/plane
Length of output: 41
Include token in the SWR cache key.
The fetcher requires token as a parameter (sent as a query parameter to the API), but the cache key only includes invitation_id. If the page loads with a valid token, then the token is changed in the URL, SWR will still return the cached response from the first token instead of fetching with the new token. Each unique token should have a separate cache entry since it represents a different authorization context.
Suggested cache-key fix
const { data: invitationDetail, error } = useSWR(
- invitation_id && slug && token && WORKSPACE_INVITATION(invitation_id.toString()),
+ invitation_id && slug && token
+ ? [WORKSPACE_INVITATION(invitation_id.toString()), slug.toString(), token.toString()]
+ : null,
invitation_id && slug && token
? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString(), token.toString())
: null
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { data: invitationDetail, error } = useSWR( | |
| invitation_id && slug && WORKSPACE_INVITATION(invitation_id.toString()), | |
| invitation_id && slug | |
| ? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString()) | |
| invitation_id && slug && token && WORKSPACE_INVITATION(invitation_id.toString()), | |
| invitation_id && slug && token | |
| ? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString(), token.toString()) | |
| : null | |
| const { data: invitationDetail, error } = useSWR( | |
| invitation_id && slug && token | |
| ? [WORKSPACE_INVITATION(invitation_id.toString()), slug.toString(), token.toString()] | |
| : null, | |
| invitation_id && slug && token | |
| ? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString(), token.toString()) | |
| : null | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/app/`(all)/workspace-invitations/page.tsx around lines 41 - 45, The
SWR cache key currently only includes invitation_id (via WORKSPACE_INVITATION)
but not token, so changing token won't cause a refetch; update the useSWR key to
include token (and slug if desired) so each distinct token has its own cache
entry — e.g., when calling useSWR ensure the first arg contains token (for
example an array or a key string that includes token) and keep the same fetcher
workspaceService.getWorkspaceInvitation(slug.toString(),
invitation_id.toString(), token.toString()) so SWR will create separate entries
per token.
| const { data: invitation, isLoading } = useSWR( | ||
| workspaceSlug && invitationId ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null, | ||
| async () => workspaceSlug && invitationId && workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId), | ||
| workspaceSlug && invitationId && invitationToken ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null, | ||
| async () => | ||
| workspaceSlug && | ||
| invitationId && | ||
| invitationToken && | ||
| workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId, invitationToken), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/web/core/components/account/auth-forms/auth-header.tsx | head -80Repository: makeplane/plane
Length of output: 3073
🏁 Script executed:
# Get the full extent of the useSWR call and understand the context
sed -n '60,80p' apps/web/core/components/account/auth-forms/auth-header.tsxRepository: makeplane/plane
Length of output: 785
🏁 Script executed:
# Check if invitationToken is a query parameter in the URL
rg "invitationToken" apps/web/core/components/account/auth-forms/auth-header.tsx -A 2 -B 2Repository: makeplane/plane
Length of output: 865
Include invitationToken in the SWR cache key.
The cache key only uses workspaceSlug and invitationId, but the API authorization requires all three parameters including invitationToken. This allows responses from one token to be reused when the same invitation is accessed with a different token within the same session, bypassing re-authorization.
Suggested cache-key fix
const { data: invitation, isLoading } = useSWR(
- workspaceSlug && invitationId && invitationToken ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null,
+ workspaceSlug && invitationId && invitationToken
+ ? ["WORKSPACE_INVITATION", workspaceSlug, invitationId, invitationToken]
+ : null,
async () =>
workspaceSlug &&
invitationId &&
invitationToken &&
workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId, invitationToken),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { data: invitation, isLoading } = useSWR( | |
| workspaceSlug && invitationId ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null, | |
| async () => workspaceSlug && invitationId && workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId), | |
| workspaceSlug && invitationId && invitationToken ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null, | |
| async () => | |
| workspaceSlug && | |
| invitationId && | |
| invitationToken && | |
| workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId, invitationToken), | |
| const { data: invitation, isLoading } = useSWR( | |
| workspaceSlug && invitationId && invitationToken | |
| ? ["WORKSPACE_INVITATION", workspaceSlug, invitationId, invitationToken] | |
| : null, | |
| async () => | |
| workspaceSlug && | |
| invitationId && | |
| invitationToken && | |
| workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId, invitationToken), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/core/components/account/auth-forms/auth-header.tsx` around lines 66
- 72, The SWR cache key used in useSWR (currently
`WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}`) omits invitationToken,
allowing stale/unauthorized responses to be reused; update the key to include
invitationToken (e.g.,
`WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}_${invitationToken}`) and
ensure the same workspaceSlug && invitationId && invitationToken condition
guards the hook so the cache differentiates requests by token for
workSpaceService.getWorkspaceInvitation.
| async getWorkspaceInvitation( | ||
| workspaceSlug: string, | ||
| invitationId: string, | ||
| token: string | ||
| ): Promise<IWorkspaceMemberInvitation> { | ||
| return this.get(`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/join/`, { | ||
| headers: {}, | ||
| params: { token }, | ||
| }) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
# Verify the method exists at the stated lines and get context
cat -n apps/web/core/services/workspace.service.ts | sed -n '165,185p'Repository: makeplane/plane
Length of output: 803
🏁 Script executed:
# Check the type definition of IWorkspaceMemberInvitation
cat packages/types/src/workspace.ts | grep -A 20 "IWorkspaceMemberInvitation"Repository: makeplane/plane
Length of output: 467
🏁 Script executed:
# Verify the backend endpoint mentioned in the review
cat apps/api/plane/app/views/workspace/invite.py | sed -n '255,275p'Repository: makeplane/plane
Length of output: 757
🏁 Script executed:
# Check all imports in workspace.service.ts to see if types are properly imported
head -50 apps/web/core/services/workspace.service.tsRepository: makeplane/plane
Length of output: 1396
🏁 Script executed:
# Find all usages of getWorkspaceInvitation
rg "getWorkspaceInvitation" --type ts --type tsx -B 2 -A 2Repository: makeplane/plane
Length of output: 87
🏁 Script executed:
# Check if there's an existing public invitation type or similar pattern in workspace.ts
rg "Invitation|Invite" packages/types/src/workspace.ts -iRepository: makeplane/plane
Length of output: 213
🏁 Script executed:
# Check the entire WorkspaceService to see if there are similar type mismatches
rg "Promise<I" apps/web/core/services/workspace.service.tsRepository: makeplane/plane
Length of output: 1756
🏁 Script executed:
# Fix the file type search - use ts for TypeScript files
rg "getWorkspaceInvitation" -t ts -B 2 -A 5Repository: makeplane/plane
Length of output: 6372
🏁 Script executed:
# Check if userWorkspaceInvitations and workspaceInvitations also have similar issues
rg "userWorkspaceInvitations|workspaceInvitations" apps/web/core/services/workspace.service.ts -B 2 -A 10Repository: makeplane/plane
Length of output: 896
🏁 Script executed:
# Find where getWorkspaceInvitation is called to understand how the return value is used
rg "getWorkspaceInvitation" --type-list | head -5Repository: makeplane/plane
Length of output: 449
🏁 Script executed:
# Search for files that call getWorkspaceInvitation
find apps/web -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "getWorkspaceInvitation" 2>/dev/nullRepository: makeplane/plane
Length of output: 346
🏁 Script executed:
# Check how the invitation data is used in workspace-invitations/page.tsx
sed -n '1,100p' apps/web/app/\(all\)/workspace-invitations/page.tsx | head -80Repository: makeplane/plane
Length of output: 2634
🏁 Script executed:
# Check the full usage of invitationDetail in workspace-invitations/page.tsx
grep -A 20 "invitationDetail" apps/web/app/\(all\)/workspace-invitations/page.tsx | head -50Repository: makeplane/plane
Length of output: 1832
🏁 Script executed:
# Check how the invitation data is used in auth-header.tsx
grep -B 5 -A 20 "getWorkspaceInvitation" apps/web/core/components/account/auth-forms/auth-header.tsxRepository: makeplane/plane
Length of output: 1026
🏁 Script executed:
# Check the backend endpoints for userWorkspaceInvitations and workspaceInvitations
# to see if they also strip token and invite_link
rg "token.*invite_link|invite_link.*token" apps/api/plane/app/views/workspace/invite.py -B 5 -A 5Repository: makeplane/plane
Length of output: 401
🏁 Script executed:
# Check other invitation-related endpoints to understand the pattern
grep -n "def.*invitation" apps/api/plane/app/views/workspace/invite.py -iRepository: makeplane/plane
Length of output: 41
Narrow this method's return type to match the public invite shape.
getWorkspaceInvitation returns IWorkspaceMemberInvitation, but the backend endpoint strips token and invite_link from the response. This breaks the type contract—callers compile against fields that are no longer returned. Update the return type using Omit to reflect the actual response shape:
Suggested fix
+type TPublicWorkspaceMemberInvitation = Omit<IWorkspaceMemberInvitation, "token" | "invite_link">;
+
async getWorkspaceInvitation(
workspaceSlug: string,
invitationId: string,
token: string
- ): Promise<IWorkspaceMemberInvitation> {
+ ): Promise<TPublicWorkspaceMemberInvitation> {
return this.get(`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/join/`, {
headers: {},
params: { token },
})Per coding guidelines: "Enable TypeScript strict mode and ensure all files must be typed."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/core/services/workspace.service.ts` around lines 172 - 180,
getWorkspaceInvitation currently declares it returns IWorkspaceMemberInvitation
but the API response omits token and invite_link; change the method's return
type to reflect that by using Omit to remove those keys (e.g.
Promise<Omit<IWorkspaceMemberInvitation, 'token' | 'invite_link'>>), update any
related imports/signatures if needed, and ensure callers are adjusted to the
narrower shape so TypeScript strict mode is satisfied; keep the
getWorkspaceInvitation implementation unchanged aside from the updated return
type.
Summary
Closes the unauthenticated invitation token leak reported in GHSA-gf48-p6jp-cwc4.
GET /api/workspaces/<slug>/invitations/<pk>/join/was registered withpermission_classes = [AllowAny]and serializedWorkspaceMemberInvitewithfields = "__all__", exposing the secret invitationtoken(plus aninvite_linkcontaining it) to any caller who could guess a workspace slug + invitation id. An attacker could iterate through invitation IDs and accept invites intended for other users.Backend (
apps/api/plane/app/views/workspace/invite.py)WorkspaceJoinEndpoint.getnow requires the?token=from the email link, compared in constant time. Missing/mismatched/non-existent invites all return a uniform403so existence cannot be probed.tokenandinvite_linkare stripped from the response as defense in depth, even for verified callers — they already have the token, so we don't echo it back.POSTbehavior is unchanged.Frontend
WorkspaceService.getWorkspaceInvitation(slug, id, token)now requires a token and forwards it as a query param./workspace-invitationspage passes the token it already extracts from the URL.AuthRootreadstokenfrom the search params and threads it intoAuthHeaderasinvitationToken. When absent,AuthHeadersimply skips the invitation-context SWR call — pages without proof of access shouldn't render invitation data anyway.no-shadow,always-return) were tidied up so the security commit could passlint-stagedwithout bypass.Test plan
GET /api/workspaces/<slug>/invitations/<pk>/join/without?token=returns 403 and no invitation data.tokenorinvite_linkin the body./workspace-invitationspage and lets the invitee accept/reject.AuthRoot→AuthHeader)./sign-inwithout invite params still works (no SWR call fires).Summary by CodeRabbit
Release Notes