VYPR
High severity8.8NVD Advisory· Published May 21, 2026

CVE-2026-47102

CVE-2026-47102

Description

LiteLLM prior to 1.83.10 allows a user to modify their own user_role via the /user/update endpoint. While the endpoint correctly restricts users to updating only their own account, it does not restrict which fields may be changed. A user who can reach this endpoint can set their role to proxy_admin, gaining full administrative access to LiteLLM including all users, teams, keys, models, and prompt history. Users with the org_admin role have legitimate access to this endpoint and can exploit this vulnerability without chaining any additional flaw.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

LiteLLM before v1.83.10 allows internal users to escalate to proxy_admin by modifying their own user_role via /user/update due to missing field-level authorization checks.

LiteLLM versions prior to 1.83.10 contain a privilege escalation vulnerability in the /user/update endpoint. The endpoint correctly restricts users to updating only their own account but fails to validate which fields can be modified. This allows an authenticated user to change their user_role to proxy_admin, gaining full administrative control over the LiteLLM proxy, including all users, teams, keys, models, and prompt history [1][3].

The vulnerability can be exploited by any user who can reach the /user/update endpoint. While internal_user roles lack direct access, the /key/generate endpoint allows creating virtual keys with arbitrary allowed_routes, including routes intended for administrators. By generating a key with access to /user/update, an attacker can escalate from an internal_user to proxy_admin [1]. Users with the org_admin role have legitimate access to this endpoint and can exploit the vulnerability without requiring any additional flaw [3].

Successfully escalating to proxy_admin grants the attacker unrestricted access to all LiteLLM proxy routes and data. This includes the ability to view and manage all users, teams, API keys, model configurations, and prompt history. The attacker can also create, modify, or delete any resource within the proxy, potentially leading to data exposure, service disruption, or further lateral movement [1].

The issue is fixed in LiteLLM release v1.83.10 [4]. The fix introduces proxy-admin-only guards for user_role changes in both single and bulk user update endpoints, and extends the existing admin-only checks on the /key/update endpoint to also cover the spend field [2][3]. Users should upgrade to v1.83.10 or later to mitigate this vulnerability.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Berriai/Litellmreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: < 1.83.10

Patches

3
128d32d2494b

fix: extend field-level checks to bulk user update path

https://github.com/BerriAI/litellmYuneng JiangApr 11, 2026via nvd-ref
1 file changed · +17 0
  • litellm/proxy/management_endpoints/internal_user_endpoints.py+17 0 modified
    @@ -1518,6 +1518,23 @@ async def bulk_user_update(
                 detail={"error": "Database not connected"},
             )
     
    +    # Only proxy admins can modify user_role in bulk updates
    +    _bulk_role = (
    +        getattr(data.user_updates, "user_role", None) if data.user_updates else None
    +    )
    +    if _bulk_role is None and data.users:
    +        _bulk_role = next(
    +            (u.user_role for u in data.users if u.user_role is not None), None
    +        )
    +    if (
    +        _bulk_role is not None
    +        and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
    +    ):
    +        raise HTTPException(
    +            status_code=403,
    +            detail="Only proxy admins can modify user roles.",
    +        )
    +
         # Determine the list of users to update
         users_to_update: Union[
             List[UpdateUserRequest], List[UpdateUserRequestNoUserIDorEmail]
    
e6f18ce75b11

fix: align field-level checks in user and key update endpoints

https://github.com/BerriAI/litellmYuneng JiangApr 10, 2026via nvd-ref
2 files changed · +37 22
  • litellm/proxy/management_endpoints/internal_user_endpoints.py+10 0 modified
    @@ -1131,6 +1131,16 @@ async def _update_single_user_helper(
         if prisma_client is None:
             raise Exception("Not connected to DB!")
     
    +    # Only proxy admins can modify user_role
    +    if (
    +        user_request.user_role is not None
    +        and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
    +    ):
    +        raise HTTPException(
    +            status_code=403,
    +            detail="Only proxy admins can modify user roles.",
    +        )
    +
         # Validate user identifier
         if not user_request.user_id and not user_request.user_email:
             raise ValueError("Either user_id or user_email must be provided")
    
  • litellm/proxy/management_endpoints/key_management_endpoints.py+27 22 modified
    @@ -768,9 +768,9 @@ async def _common_key_generation_helper(  # noqa: PLR0915
             request_type="key", **data_json, table_name="key"
         )
     
    -    response["soft_budget"] = (
    -        data.soft_budget
    -    )  # include the user-input soft budget in the response
    +    response[
    +        "soft_budget"
    +    ] = data.soft_budget  # include the user-input soft budget in the response
     
         response = GenerateKeyResponse(**response)
     
    @@ -1961,16 +1961,21 @@ async def _validate_update_key_data(
             user_api_key_cache=user_api_key_cache,
         )
     
    -    # Admin-only: only proxy admins, team admins, or org admins can modify max_budget
    -    if data.max_budget is not None and data.max_budget != existing_key_row.max_budget:
    +    # Admin-only: only proxy admins, team admins, or org admins can modify max_budget or spend
    +    if (
    +        data.max_budget is not None and data.max_budget != existing_key_row.max_budget
    +    ) or (
    +        data.spend is not None
    +        and data.spend != getattr(existing_key_row, "spend", None)
    +    ):
             if prisma_client is not None:
                 hashed_key = existing_key_row.token
                 await _check_key_admin_access(
                     user_api_key_dict=user_api_key_dict,
                     hashed_token=hashed_key,
                     prisma_client=prisma_client,
                     user_api_key_cache=user_api_key_cache,
    -                route="/key/update (max_budget)",
    +                route="/key/update (max_budget/spend)",
                 )
     
         # Check team limits if key has a team_id (from request or existing key)
    @@ -3272,10 +3277,10 @@ async def delete_verification_tokens(
         try:
             if prisma_client:
                 tokens = [_hash_token_if_needed(token=key) for key in tokens]
    -            _keys_being_deleted: List[LiteLLM_VerificationToken] = (
    -                await prisma_client.db.litellm_verificationtoken.find_many(
    -                    where={"token": {"in": tokens}}
    -                )
    +            _keys_being_deleted: List[
    +                LiteLLM_VerificationToken
    +            ] = await prisma_client.db.litellm_verificationtoken.find_many(
    +                where={"token": {"in": tokens}}
                 )
     
                 if len(_keys_being_deleted) == 0:
    @@ -3475,9 +3480,9 @@ async def _rotate_master_key(  # noqa: PLR0915
         from litellm.proxy.proxy_server import proxy_config
     
         try:
    -        models: Optional[List] = (
    -            await prisma_client.db.litellm_proxymodeltable.find_many()
    -        )
    +        models: Optional[
    +            List
    +        ] = await prisma_client.db.litellm_proxymodeltable.find_many()
         except Exception:
             models = None
         # 2. process model table
    @@ -4117,11 +4122,11 @@ async def validate_key_list_check(
                 param="user_id",
                 code=status.HTTP_403_FORBIDDEN,
             )
    -    complete_user_info_db_obj: Optional[BaseModel] = (
    -        await prisma_client.db.litellm_usertable.find_unique(
    -            where={"user_id": user_api_key_dict.user_id},
    -            include={"organization_memberships": True},
    -        )
    +    complete_user_info_db_obj: Optional[
    +        BaseModel
    +    ] = await prisma_client.db.litellm_usertable.find_unique(
    +        where={"user_id": user_api_key_dict.user_id},
    +        include={"organization_memberships": True},
         )
     
         if complete_user_info_db_obj is None:
    @@ -4204,10 +4209,10 @@ async def _fetch_user_team_objects(
         if complete_user_info is None or not complete_user_info.teams:
             return []
     
    -    teams: Optional[List[BaseModel]] = (
    -        await prisma_client.db.litellm_teamtable.find_many(
    -            where={"team_id": {"in": complete_user_info.teams}}
    -        )
    +    teams: Optional[
    +        List[BaseModel]
    +    ] = await prisma_client.db.litellm_teamtable.find_many(
    +        where={"team_id": {"in": complete_user_info.teams}}
         )
         if teams is None:
             return []
    
05e9ca7e75ed

Merge pull request #25541 from BerriAI/litellm_field_level_checks

https://github.com/BerriAI/litellmyuneng-jiangApr 12, 2026via nvd-ref
2 files changed · +60 22
  • litellm/proxy/management_endpoints/internal_user_endpoints.py+33 0 modified
    @@ -1131,6 +1131,16 @@ async def _update_single_user_helper(
         if prisma_client is None:
             raise Exception("Not connected to DB!")
     
    +    # Only proxy admins can modify user_role
    +    if (
    +        user_request.user_role is not None
    +        and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
    +    ):
    +        raise HTTPException(
    +            status_code=403,
    +            detail="Only proxy admins can modify user roles.",
    +        )
    +
         # Validate user identifier
         if not user_request.user_id and not user_request.user_email:
             raise ValueError("Either user_id or user_email must be provided")
    @@ -1508,12 +1518,35 @@ async def bulk_user_update(
                 detail={"error": "Database not connected"},
             )
     
    +    # Only proxy admins can modify user_role in bulk updates
    +    _bulk_role = (
    +        getattr(data.user_updates, "user_role", None) if data.user_updates else None
    +    )
    +    if _bulk_role is None and data.users:
    +        _bulk_role = next(
    +            (u.user_role for u in data.users if u.user_role is not None), None
    +        )
    +    if (
    +        _bulk_role is not None
    +        and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
    +    ):
    +        raise HTTPException(
    +            status_code=403,
    +            detail="Only proxy admins can modify user roles.",
    +        )
    +
         # Determine the list of users to update
         users_to_update: Union[
             List[UpdateUserRequest], List[UpdateUserRequestNoUserIDorEmail]
         ] = []
     
         if data.all_users and data.user_updates:
    +        # Only proxy admins can update all users at once
    +        if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:
    +            raise HTTPException(
    +                status_code=403,
    +                detail="Only proxy admins can update all users at once.",
    +            )
             # Optimized path for updating all users directly in database
             all_users_in_db = await prisma_client.db.litellm_usertable.find_many(
                 order={"created_at": "desc"}
    
  • litellm/proxy/management_endpoints/key_management_endpoints.py+27 22 modified
    @@ -768,9 +768,9 @@ async def _common_key_generation_helper(  # noqa: PLR0915
             request_type="key", **data_json, table_name="key"
         )
     
    -    response["soft_budget"] = (
    -        data.soft_budget
    -    )  # include the user-input soft budget in the response
    +    response[
    +        "soft_budget"
    +    ] = data.soft_budget  # include the user-input soft budget in the response
     
         response = GenerateKeyResponse(**response)
     
    @@ -1961,16 +1961,21 @@ async def _validate_update_key_data(
             user_api_key_cache=user_api_key_cache,
         )
     
    -    # Admin-only: only proxy admins, team admins, or org admins can modify max_budget
    -    if data.max_budget is not None and data.max_budget != existing_key_row.max_budget:
    +    # Admin-only: only proxy admins, team admins, or org admins can modify max_budget or spend
    +    if (
    +        data.max_budget is not None and data.max_budget != existing_key_row.max_budget
    +    ) or (
    +        data.spend is not None
    +        and data.spend != getattr(existing_key_row, "spend", None)
    +    ):
             if prisma_client is not None:
                 hashed_key = existing_key_row.token
                 await _check_key_admin_access(
                     user_api_key_dict=user_api_key_dict,
                     hashed_token=hashed_key,
                     prisma_client=prisma_client,
                     user_api_key_cache=user_api_key_cache,
    -                route="/key/update (max_budget)",
    +                route="/key/update (max_budget/spend)",
                 )
     
         # Check team limits if key has a team_id (from request or existing key)
    @@ -3272,10 +3277,10 @@ async def delete_verification_tokens(
         try:
             if prisma_client:
                 tokens = [_hash_token_if_needed(token=key) for key in tokens]
    -            _keys_being_deleted: List[LiteLLM_VerificationToken] = (
    -                await prisma_client.db.litellm_verificationtoken.find_many(
    -                    where={"token": {"in": tokens}}
    -                )
    +            _keys_being_deleted: List[
    +                LiteLLM_VerificationToken
    +            ] = await prisma_client.db.litellm_verificationtoken.find_many(
    +                where={"token": {"in": tokens}}
                 )
     
                 if len(_keys_being_deleted) == 0:
    @@ -3475,9 +3480,9 @@ async def _rotate_master_key(  # noqa: PLR0915
         from litellm.proxy.proxy_server import proxy_config
     
         try:
    -        models: Optional[List] = (
    -            await prisma_client.db.litellm_proxymodeltable.find_many()
    -        )
    +        models: Optional[
    +            List
    +        ] = await prisma_client.db.litellm_proxymodeltable.find_many()
         except Exception:
             models = None
         # 2. process model table
    @@ -4117,11 +4122,11 @@ async def validate_key_list_check(
                 param="user_id",
                 code=status.HTTP_403_FORBIDDEN,
             )
    -    complete_user_info_db_obj: Optional[BaseModel] = (
    -        await prisma_client.db.litellm_usertable.find_unique(
    -            where={"user_id": user_api_key_dict.user_id},
    -            include={"organization_memberships": True},
    -        )
    +    complete_user_info_db_obj: Optional[
    +        BaseModel
    +    ] = await prisma_client.db.litellm_usertable.find_unique(
    +        where={"user_id": user_api_key_dict.user_id},
    +        include={"organization_memberships": True},
         )
     
         if complete_user_info_db_obj is None:
    @@ -4204,10 +4209,10 @@ async def _fetch_user_team_objects(
         if complete_user_info is None or not complete_user_info.teams:
             return []
     
    -    teams: Optional[List[BaseModel]] = (
    -        await prisma_client.db.litellm_teamtable.find_many(
    -            where={"team_id": {"in": complete_user_info.teams}}
    -        )
    +    teams: Optional[
    +        List[BaseModel]
    +    ] = await prisma_client.db.litellm_teamtable.find_many(
    +        where={"team_id": {"in": complete_user_info.teams}}
         )
         if teams is None:
             return []
    

Vulnerability mechanics

Root cause

"Missing field-level access control on the /user/update endpoint allows a user to modify their own user_role field to any value, including proxy_admin."

Attack vector

An authenticated user with any role (including a low-privilege user) sends a PUT/PATCH request to the /user/update endpoint with their own user_id and sets the user_role field to "proxy_admin". The endpoint validates that the user can only update their own account but does not restrict which fields can be changed [patch_id=1264293][patch_id=1264294][patch_id=1264295]. No special network position is required beyond normal API access. The attacker then has full administrative privileges over all users, teams, keys, models, and prompt history.

Affected code

The /user/update endpoint in LiteLLM prior to 1.83.10 lacks field-level access control. The endpoint correctly restricts users to updating only their own account record, but does not validate which fields can be modified. Specifically, the user_role field is not protected from self-modification by non-admin users [patch_id=1264293][patch_id=1264294][patch_id=1264295].

What the fix does

The patches add field-level validation to the /user/update endpoint to prevent non-admin users from modifying the user_role field. The fix checks the requesting user's current role and only allows role changes when the requester already holds proxy_admin or org_admin privileges [patch_id=1264293][patch_id=1264294][patch_id=1264295]. This closes the privilege escalation vector by ensuring that a user cannot self-promote to a higher role through the update endpoint.

Preconditions

  • authAttacker must have a valid authenticated session with LiteLLM
  • networkAttacker must be able to reach the /user/update API endpoint over the network
  • inputAttacker must supply their own user_id and set user_role to 'proxy_admin' in the request body

Generated on May 21, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

47