Skip to content

[Fix] Invite link allows multiple password resets - #22462

Merged
yuneng-jiang merged 1 commit into
mainfrom
litellm_invite_link_reuse_fix
Mar 1, 2026
Merged

yuneng-jiang merged 1 commit into
mainfrom
litellm_invite_link_reuse_fix

Conversation

@yuneng-jiang

@yuneng-jiang yuneng-jiang commented Mar 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #13906

Summary

Failure Path (Before Fix)

Invite links could be reused indefinitely to overwrite a user's password. The GET /onboarding/get_token endpoint set is_accepted = True when the link was first visited, but POST /onboarding/claim_token treated is_accepted = True as the allow condition rather than a block condition. Since the flag was never reset after a password was set, any subsequent visit to the same link allowed a new password to be submitted, invalidating the previous one.

Fix

Moved is_accepted = True from GET /onboarding/get_token to POST /onboarding/claim_token, so the flag now accurately means "a password has been set via this link." Both endpoints check the flag and reject the request with 401 if it is already True. The GET endpoint rejects before querying the user table, so no user data (email, role) is ever returned for an already-used link.

Testing

Added tests/test_litellm/proxy/auth/test_onboarding.py with 8 unit tests covering:

  • GET /onboarding/get_token: rejects already-used links (401 before user data is read), expired links, and missing links; confirms is_accepted is not written on valid first visit
  • POST /onboarding/claim_token: rejects already-used links (password never written), expired links, mismatched user IDs; confirms password is written and is_accepted = True is set only after a successful first claim

Type

🐛 Bug Fix
✅ Test

Moves is_accepted=True from GET /onboarding/get_token to POST /onboarding/claim_token,
so the flag accurately reflects that a password has been set. Both endpoints now reject
already-used links, with get_token rejecting before any user data is returned.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Mar 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Building Building Preview, Comment Mar 1, 2026 1:39am

Request Review

@greptile-apps

greptile-apps Bot commented Mar 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a security vulnerability where invite links could be reused indefinitely to overwrite a user's password. The root cause was that GET /onboarding/get_token set is_accepted = True on first visit, but POST /onboarding/claim_token treated that flag as an allow condition instead of a block condition.

  • Security fix: Moved is_accepted = True from GET /onboarding/get_token to POST /onboarding/claim_token, so the flag now means "password has been set." Both endpoints reject requests with 401 when is_accepted is already True.
  • Data leak prevention: The GET endpoint now checks is_accepted before querying the user table, so no user data is leaked for already-used links.
  • Tests: Added 8 mock-only unit tests covering all rejection paths (used links, expired links, missing links, user-id mismatch) and verifying the correct ordering of password write → flag update.
  • Minor note: A small TOCTOU race window remains between checking is_accepted and setting it in claim_token due to intervening await points, but this is a massive improvement over the prior unlimited-reuse behavior.

Confidence Score: 4/5

  • This PR is a clear security improvement that is safe to merge; the remaining TOCTOU race window is narrow and non-critical.
  • The logic change is straightforward and correct — it moves the flag-setting to the right place and adds proper guards. Tests are comprehensive and mock-only. The only concern is a minor race condition on concurrent requests, which doesn't block merging but is worth a follow-up.
  • litellm/proxy/proxy_server.py — stale docstring on the onboarding function and a minor TOCTOU race in claim_onboarding_link.

Important Files Changed

Filename Overview
litellm/proxy/proxy_server.py Security fix: moves is_accepted=True from GET to POST endpoint so invite links can only set a password once. Minor TOCTOU race window exists between check and update, and the GET endpoint docstring is stale.
tests/test_litellm/proxy/auth/test_onboarding.py New test file with 8 well-structured mock-only unit tests covering both endpoints for rejection of used/expired/missing links, user-id mismatch, and correct flag-setting order. No real network calls.

Sequence Diagram

sequenceDiagram
    participant U as User (Browser)
    participant GET as GET /onboarding/get_token
    participant POST as POST /onboarding/claim_token
    participant DB as Database

    U->>GET: Visit invite link
    GET->>DB: Find invite by ID
    DB-->>GET: invite_obj
    GET->>GET: Check expired? Check is_accepted?
    alt is_accepted = True
        GET-->>U: 401 "already been used"
    else valid & unused
        GET->>DB: Find user by user_id
        DB-->>GET: user_obj
        GET-->>U: {token, login_url, user_email}
    end

    U->>POST: Submit password (invitation_link, user_id, password)
    POST->>DB: Find invite by ID
    DB-->>POST: invite_obj
    POST->>POST: Check expired? Check is_accepted? Check user_id match?
    alt is_accepted = True
        POST-->>U: 401 "already been used"
    else valid & unused
        POST->>DB: Update user password (hashed)
        DB-->>POST: user_obj
        POST->>DB: Set is_accepted = True on invite
        POST-->>U: user_obj (success)
    end
Loading

Last reviewed commit: c2e7cf1

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 10937 to +10975
@@ -10976,6 +10962,18 @@ async def claim_onboarding_link(data: InvitationClaim):
status_code=401, detail={"error": "User does not exist in db."}
)

#### MARK LINK AS USED
current_time = litellm.utils.get_utc_datetime()
await prisma_client.db.litellm_invitationlink.update(
where={"id": data.invitation_link},
data={
"accepted_at": current_time,
"updated_at": current_time,
"is_accepted": True,
"updated_by": invite_obj.user_id, # type: ignore
},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TOCTOU race on is_accepted flag

There is a time-of-check-time-of-use window between reading is_accepted (line 10938) and setting it to True (line 10967–10975). Because there are multiple await points in between (the user table update on line 10956), two concurrent POST /onboarding/claim_token requests for the same invite link could both pass the is_accepted check and both successfully overwrite the password.

This is a significant improvement over the previous unlimited-reuse bug, but for a security-critical path you could tighten this further by marking the link as used before writing the password (and rolling back if the password write fails), or by using an atomic conditional update like:

# Atomically set is_accepted = True only if currently False
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, ...},
)
if result.count == 0:
    raise HTTPException(status_code=401, detail={"error": "Invitation link has already been used."})

This eliminates the race window entirely.

@greptile-apps

greptile-apps Bot commented Mar 1, 2026

Copy link
Copy Markdown
Contributor
Additional Comments (1)

litellm/proxy/proxy_server.py
Stale docstring after behavior change

The docstring still says "Invalidate the link (prevents abuse)", but this endpoint no longer invalidates the link — that responsibility moved to claim_onboarding_link. Consider updating to accurately describe the new behavior (e.g., "Validate the link is still unused and not expired").

    """
    - Get the invite link
    - Validate it's still 'valid' and unused
    - Get user from db
    - Pass in user_email if set
    """

@yuneng-jiang
yuneng-jiang merged commit 121c633 into main Mar 1, 2026
60 of 95 checks passed
@ishaan-berri
ishaan-berri deleted the litellm_invite_link_reuse_fix branch March 26, 2026 22:29
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…se_fix

[Fix] Invite link allows multiple password resets

This branch had an error being deployed

1 failed deployment
Preview — c2e7cf16 Deployed Mar 1, 2026 by vercel[bot]
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