Skip to content

Conversation

@ricdikulous
Copy link

Title

Move invitation acceptance tracking to claim endpoint and implement atomic update to prevent reuse

Relevant issues

Fixes #13906 - Invite link allows password reset multiple times, leading to security vulnerability

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • I have added a screenshot of my new test passing locally
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem

Type

✅ Test
🐛 Bug Fix
🔒 Security
🧹 Refactoring

Changes

Summary

Fixes a security vulnerability where invitation links could be used multiple times to reset passwords. Moved the is_accepted flag update from the GET endpoint to the POST endpoint and implemented atomic database operations to prevent race conditions.

What Changed

1. Moved is_accepted update from GET to POST endpoint

  • Previously: is_accepted was set to True in /onboarding/get_token (GET endpoint) when the JWT token was generated
  • Now: is_accepted is set to True in /onboarding/claim_token (POST endpoint) when the password is actually set
  • Why this matters: The invitation is only marked as "used" when the password is actually claimed, not just when the token is retrieved
  • Benefit: Allows both the endpoints to check is_accepted status and reject already-used invitations early

2. Implemented atomic is_accepted update in POST endpoint

  • Uses atomic update_many with compound WHERE clause: WHERE id = ? AND is_accepted = False
  • This ensures the invitation can only be accepted once, even under race conditions
  • Returns count of updated rows (1 = success, 0 = already accepted)
  • If update_result == 0, raises 401 error indicating the link has already been used

3. Added early validation check in POST endpoint

  • Added is_accepted check before attempting atomic update
  • Provides immediate feedback when invitation has already been used
  • Prevents unnecessary operations (password hashing, database updates)
  • Returns clear 401 error with descriptive message

4. Added comprehensive test coverage

  • Added 4 unit tests for both onboarding endpoints:
    • test_onboarding_get_token_happy_path - Valid token generation flow
    • test_onboarding_get_token_already_accepted - GET endpoint rejects already-used invitations
    • test_claim_onboarding_link_happy_path - Valid password claim flow with atomic update
    • test_claim_onboarding_link_already_accepted - POST endpoint rejects already-claimed invitations

Security Impact

  • Prevents multiple password resets from the same invitation link
  • Atomic operation prevents race conditions - two simultaneous claim requests will result in only one successful password update
  • Better separation of concerns - invitation is marked as used only when password is actually set
  • Defense in Depth: Early check + atomic database operation provide two layers of protection

Technical Details

Flow Changes

Before (Vulnerable):

  1. GET /onboarding/get_token:
    • Generate JWT token
    • ❌ Mark invitation as accepted (is_accepted = True)
  2. POST /onboarding/claim_token:
    • Update password
    • ⚠️ Could NOT check is_accepted because it was already set in GET
    • ⚠️ Link could be reused to reset password multiple times

After (Secure):

  1. GET /onboarding/get_token:
    • Generate JWT token
    • ✅ Does NOT mark invitation as accepted
    • ✅ Can check is_accepted to reject already-used invitations
  2. POST /onboarding/claim_token:
    • ✅ Check if already accepted (early validation)
    • Atomically mark invitation as accepted (prevents race conditions)
    • ✅ Update password only if atomic update succeeded
    • ✅ Link can only be used once

Atomic Update Implementation

# Use update_many with compound WHERE clause to prevent race conditions
# Only updates if is_accepted = False, ensuring one-time use
update_result = await prisma_client.db.litellm_invitationlink.update_many(
    where={"id": data.invitation_link, "is_accepted": False},
    data={
        "is_accepted": True,
        "accepted_at": current_time,
        "updated_at": current_time,
        "updated_by": invite_obj.user_id,
    },
)

# If no rows were updated, the invitation was already accepted (race condition caught)
if update_result == 0:
    raise HTTPException(status_code=401, detail={"error": "..."})

…Also prevent race conditions in onboarding invitation acceptance with atomic updates
@vercel
Copy link

vercel bot commented Oct 17, 2025

@ricdikulous is attempting to deploy a commit to the CLERKIEAI Team on Vercel.

A member of the Team first needs to authorize it.

@ricdikulous
Copy link
Author

@ishaan-jaff is there any chance of getting this reviewed? Our cyber sec team raised this as a serious issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Invite link allows password reset multiple times, leading to security vulnerability

1 participant