fix(customer_seat): reject seat claim when linked member is soft-deleted - #14170
Merged
psincraian merged 1 commit intoSep 8, 2026
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
OpenAPI ChangesNo changes detected in the OpenAPI schema. |
psincraian
approved these changes
Sep 8, 2026
psincraian
deleted the
detail/bug-fix/fix-customer-seat-reject-seat-claim-when-linked-me-04882f
branch
September 8, 2026 14:58
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Detail bug report: View on Detail
Summary
Related Issue: polarsource/feedback#436
Customer-portal seat claims with a soft-deleted linked member in
member_model_enabledorganizations issued apolar_cst_customer-portal session for the billing/purchaser customer instead of the seat-scoped member — a privilege escalation that granted the seat-claimercustomer_portal_writeas the purchaser and bypassed theowner/billing_managerrole gate on billing endpoints (cancel/update/revoke subscriptions, invoice/retry orders, request email change).What
server/polar/customer_seat/service.pyclaim_seatwas rewritten in #9983 to dispatch the session type onseat.member_idpresence and fall back tocreate_customer_session(session, session_customer)when the linked member was missing. In member-model modesession_customeris the billing customer, so that fallback minted a purchaser-scope session. Restored themember_model_enabled andouter guard and replaced the fallbackcreate_customer_sessioncall withraise InvalidInvitationToken(invitation_token)when the linked member is not found. Legacy mode is unchanged — theresession_customeris the seat-holder's own customer, so the fallback remains a self-session (not an escalation).Added two regression tests in
server/tests/customer_seat/test_service.py::TestClaimSeatthat lock in the asymmetry the fix deliberately creates:polar_cst_self-session (not the purchaser's)Why
The escalation was introduced in #9983 (commit 96c3c17), which dropped the
member_model_enabled andguard that was previously present in #9350 and replaced the saferaise InvalidInvitationTokenwith thecreate_customer_sessionfallback. The fallback looks benign but upgrades scope in the member-model branch because the only customer available there is the purchaser. Exploitation only needs an admin to soft-delete a member while its seat's 24h invitation is still outstanding and the asynccustomer_seat.revoke_seats_for_memberjob has not yet run — a routine post-delete race window.How
Restored the exact control flow from #9350:
if member_model_enabled and seat.member_id is not None:branches into the member lookup; a found member issues apolar_mst_member session, a missing member raisesInvalidInvitationTokenwith a comment explaining why (issuing a customer session here would hand the seat-claimer purchaser-scope access). Theelsebranch keeps the legacycreate_customer_session(session, session_customer)path unchanged.Checklist
uv run task lint && uv run task lint_types)Testing
tests/customer_seat/,tests/member/,tests/customer_session/,tests/member_session/,tests/customer_portal/all pass (157 + 194 + 282 tests) — no collateral regression in the seat, session, or billing-endpoint blast radius.ruff format --check,ruff check, mypy (2-file and fulltask lint_typesover 1631 files), and thelint_org_scope/lint_subquery/lint_astAST linters all pass.app.openapi()md5 match) — endpoint shape, status codes, and schemas unchanged;alembic checkreports no schema drift (no migration required).invitation_tokenwhose member was then soft-deleted — the post-delete / pre-revoke-job race window), thencurl POST /v1/customer-seats/claimreturned HTTP 400InvalidInvitationTokenwith no session token. (Verified at the HTTP layer; an equivalent in-process HTTP regression test was written and excluded from the final diff as duplicative of the service-level test.)invitation_tokenis covered by the existingTestRevokeSeatsuite andtests/member/async-job tests.Automatic Fixes PRs can be configured here.