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
2Patches
3128d32d2494bfix: extend field-level checks to bulk user update path
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]
e6f18ce75b11fix: align field-level checks in user and key update endpoints
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 []
05e9ca7e75edMerge pull request #25541 from BerriAI/litellm_field_level_checks
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- gist.github.com/13ph03nix/9ec616e1fdc77b3673509c60206e827fnvd
- github.com/BerriAI/litellm/commit/128d32d2494b759c5d15da3452452af4c6a34c01nvd
- github.com/BerriAI/litellm/commit/e6f18ce75b111c9b93dc15c72894cbdeb53177cenvd
- github.com/BerriAI/litellm/pull/25541nvd
- github.com/BerriAI/litellm/releases/tag/v1.83.10-stablenvd
- huntr.com/bounties/8e75edfb-ff05-4e63-bfca-2d93d03fb3b9nvd
- www.vulncheck.com/advisories/litellm-privilege-escalation-via-user-updatenvd
News mentions
47- The npm Threat Landscape: Attack Surface and Mitigations (Updated May 21)Unit 42 · May 21, 2026
- GitHub confirms being hacked by TeamPCP, says customer data unaffectedThe Record · May 20, 2026
- GitHub Confirms Breach of Internal Repositories Via Malicious VS Code ExtensionInfosecurity Magazine · May 20, 2026
- TeamPCP breached GitHub’s internal codebase via poisoned VS Code extensionHelp Net Security · May 20, 2026
- GitHub investigates internal repositories breach claimed by TeamPCPBleepingComputer · May 20, 2026
- Security Researchers Find 47 Zero-Days at Pwn2Own BerlinInfosecurity Magazine · May 18, 2026
- Hackers Earn $1.3 Million at Pwn2Own Berlin 2026SecurityWeek · May 18, 2026
- TanStack Supply Chain Attack Hits Two OpenAI Employee Devices, Forces macOS UpdatesThe Hacker News · May 15, 2026
- OpenAI asks macOS users to update after TanStack npm supply chain attackThe Record · May 14, 2026
- Windows 11 and Microsoft Edge hacked at Pwn2Own Berlin 2026BleepingComputer · May 14, 2026
- Analyzing TeamPCP’s Supply Chain Attacks: Checkmarx KICS and elementary-data in CI/CD Credential TheftTrend Micro Research · May 13, 2026
- Build Application Firewalls Aim to Stop the Next Supply Chain AttackSecurityWeek · May 11, 2026
- Google researchers uncover criminal zero-day exploit likely built with AIHelp Net Security · May 11, 2026
- Adversaries Leverage AI for Vulnerability Exploitation, Augmented Operations, and Initial AccessMandiant Threat Intelligence · May 11, 2026
- PCPJack Campaign Boots TeamPCP Off Compromised MachinesInfosecurity Magazine · May 8, 2026
- MetInfo, Weaver E-cology Vulnerabilities in Attackers’ CrosshairsSecurityWeek · May 5, 2026
- Trellix Source Code Repository BreachedSecurityWeek · May 4, 2026
- TeamPCP Weekly Analysis: 2026-W18 (2026-04-27 through 2026-05-03), (Mon, May 4th)SANS Internet Storm Center · May 4, 2026
- ⚡ Weekly Recap: AI-Powered Phishing, Android Spying Tool, Linux Exploit, GitHub RCE & MoreThe Hacker News · May 4, 2026
- 4th May – Threat Intelligence ReportCheck Point Research · May 4, 2026
- Over 40,000 Servers Compromised in Ongoing cPanel ExploitationSecurityWeek · May 4, 2026
- Quasar Linux (QLNX) – A Silent Foothold in the Supply Chain: Inside a Full-Featured Linux RAT With Rootkit, PAM Backdoor, Credential Harvesting CapabilitiesTrend Micro Research · May 4, 2026
- Cisco Releases Open Source Tool for AI Model ProvenanceSecurityWeek · May 1, 2026
- The never-ending supply chain attacks worm into SAP npm packages, other dev toolsThe Register Security · Apr 30, 2026
- Great responsibility, without great powerCisco Talos Intelligence · Apr 30, 2026
- PyTorch Lightning and Intercom-client Hit in Supply Chain Attacks to Steal CredentialsThe Hacker News · Apr 30, 2026
- Vect 2.0 Ransomware Acts as Wiper, Thanks to Design ErrorDark Reading · Apr 29, 2026
- Critical Flaw Turns Vect Ransomware into Data Destroying WiperInfosecurity Magazine · Apr 29, 2026
- LiteLLM CVE-2026-42208 SQL Injection Exploited within 36 Hours of DisclosureThe Hacker News · Apr 29, 2026
- Don't pay Vect a ransom - your data's likely already wiped outThe Register Security · Apr 28, 2026
- VECT: Ransomware by design, Wiper by accidentCheck Point Research · Apr 28, 2026
- Ongoing supply-chain attack 'explicitly targeting' security, dev toolsThe Register Security · Apr 27, 2026
- Hypersonic Supply Chain Attacks: One Solution That Didn’t Need to Know the PayloadSentinelOne Labs · Apr 22, 2026
- The Vercel Breach: OAuth Supply Chain Attack Exposes the Hidden Risk in Platform Environment VariablesTrend Micro Research · Apr 20, 2026
- Frontier AI Reinforces the Future of Modern Cyber DefenseSentinelOne Labs · Apr 16, 2026
- The Q1 vulnerability pulseCisco Talos Intelligence · Apr 16, 2026
- The Good, the Bad and the Ugly in Cybersecurity – Week 14SentinelOne Labs · Apr 3, 2026
- TeamPCP Explores Ways to Exploit Stolen Supply Chain SecretsInfosecurity Magazine · Mar 31, 2026
- North Korea-Nexus Threat Actor Compromises Widely Used Axios NPM Package in Supply Chain AttackMandiant Threat Intelligence · Mar 31, 2026
- 30th March – Threat Intelligence ReportCheck Point Research · Mar 30, 2026
- TeamPCP’s Telnyx Attack Marks a Shift in Tactics Beyond LiteLLMTrend Micro Research · Mar 30, 2026
- TeamPCP Targets Telnyx Package in Latest PyPI Software Supply Chain AttackInfosecurity Magazine · Mar 27, 2026
- Your AI Gateway Was a Backdoor: Inside the LiteLLM Supply Chain CompromiseTrend Micro Research · Mar 26, 2026
- TeamPCP Expands Supply Chain Campaign With LiteLLM PyPI CompromiseInfosecurity Magazine · Mar 25, 2026
- Risky Business #830 -- LiteLLM and security scanner supply chains compromisedRisky Business · Mar 25, 2026
- Your AI Stack Just Handed Over Your Root Keys: Inside the litellm PyPI BreachTrend Micro Research · Mar 25, 2026
- CISA Adds One Known Exploited Vulnerability to CatalogCISA Alerts