CVE-2026-47101
Description
LiteLLM prior to 1.83.14 allows an authenticated internal_user to create API keys with access to routes that their role does not permit. When generating a key, the allowed_routes field is stored without verifying that the specified routes fall within the user's own permissions. A key created with access to admin-only routes can then be used to reach those routes successfully, bypassing the role-based access controls that would otherwise block the request, enabling full privilege escalation from internal_user to proxy_admin.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
LiteLLM before 1.83.14 allows an internal_user to create API keys with admin-only routes due to missing permission checks, enabling full privilege escalation to proxy_admin.
A privilege escalation vulnerability in LiteLLM prior to version 1.83.14 allows an authenticated internal_user to create API keys with arbitrary allowed_routes, including routes restricted to proxy_admin role. The /key/generate endpoint fails to verify that the specified routes fall within the user's own permissions when storing the key in the LiteLLM_VerificationToken table [2]. This bypasses the role-based access control enforced by non_proxy_admin_allowed_routes_check() in route_checks.py [2].
An attacker with internal_user privileges can generate a key with allowed_routes set to admin-only endpoints such as /user/update. Using this key, they can modify their own user_role to proxy_admin, effectively escalating privileges to full administrative control [2]. The exploitation requires only that the attacker has an authenticated session with key management permissions, which is standard for internal_user [2].
The impact is complete compromise of the LiteLLM proxy: the attacker gains access to all administrative routes, including user management, team configuration, and proxy settings. This can lead to data exfiltration, service disruption, or lateral movement within the infrastructure [2] [3] [4].
The vulnerability is fixed in release v1.83.14-stable (published May 21, 2026) [1]. The fix adds a _check_allowed_routes_caller_permission function that restricts setting allowed_routes to only proxy_admin users, with an exception for safe presets like llm_api_routes and info_routes [3] [4]. Users are advised to upgrade immediately; no workaround is available for older versions.
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
35190bd07eb23fix: extend caller-permission checks to service-account + harden raw-body acceptance
1 file changed · +33 −6
litellm/proxy/management_endpoints/key_management_endpoints.py+33 −6 modified@@ -462,22 +462,29 @@ def handle_key_type(data: GenerateKeyRequest, data_json: dict) -> dict: def _check_allowed_routes_caller_permission( allowed_routes: Optional[list], user_api_key_dict: UserAPIKeyAuth, + *, + allow_safe_presets: bool = False, ) -> None: """ - Only proxy admins may set `allowed_routes` on a key, except for the safe - presets produced by `handle_key_type` for non-elevated buckets - (`llm_api_routes`, `info_routes`). + Only proxy admins may set `allowed_routes` on a key. `allowed_routes` overrides the standard role-based route gate in RouteChecks.non_proxy_admin_allowed_routes_check, so the field is - restricted to admins outside of those safe presets. + restricted to admins. Non-admins must instead use `key_type` to pick a + preset bucket — that path goes through `handle_key_type` and re-enters + this function with `allow_safe_presets=True`, which lets the derived + `llm_api_routes` / `info_routes` values through. Raw-body call sites + leave `allow_safe_presets=False` so non-admins can't write those values + directly. """ # Empty list is the default on GenerateKeyRequest — treat as "not set". if not allowed_routes: return if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value: return - if all(r in _NON_ADMIN_SAFE_ALLOWED_ROUTES_PRESETS for r in allowed_routes): + if allow_safe_presets and all( + r in _NON_ADMIN_SAFE_ALLOWED_ROUTES_PRESETS for r in allowed_routes + ): return raise HTTPException( status_code=403, @@ -712,10 +719,13 @@ async def _common_key_generation_helper( # noqa: PLR0915 # Re-check allowed_routes after handle_key_type, since key_type can derive # an elevated bucket (e.g. ["management_routes"]) that wasn't present in - # the original request body. + # the original request body. The safe presets produced by handle_key_type + # for non-elevated buckets are accepted here; the raw-body pre-checks at + # the entry of each handler keep their default strictness. _check_allowed_routes_caller_permission( allowed_routes=data_json.get("allowed_routes"), user_api_key_dict=user_api_key_dict, + allow_safe_presets=True, ) # if we get max_budget passed to /key/generate, then use it as key_max_budget. Since generate_key_helper_fn is used to make new users @@ -1499,6 +1509,15 @@ async def generate_service_account_key_fn( detail={"error": CommonProxyErrors.db_not_connected_error.value}, ) + _check_allowed_routes_caller_permission( + allowed_routes=data.allowed_routes, + user_api_key_dict=user_api_key_dict, + ) + _check_passthrough_routes_caller_permission( + data=data, + user_api_key_dict=user_api_key_dict, + ) + await validate_team_id_used_in_service_account_request( team_id=data.team_id, prisma_client=prisma_client, @@ -3950,6 +3969,14 @@ async def regenerate_key_fn( # noqa: PLR0915 data=data, user_api_key_dict=user_api_key_dict, ) + # Mirror /key/generate's post-handle_key_type recheck so a + # non-admin can't elevate via a key_type preset that the + # regenerate flow would otherwise carry through unchecked. + _check_allowed_routes_caller_permission( + allowed_routes=handle_key_type(data, {}).get("allowed_routes"), + user_api_key_dict=user_api_key_dict, + allow_safe_presets=True, + ) is_master_key_regeneration = data and data.new_master_key is not None
2220f3076ac8fix: tighten caller-permission checks on key route fields
1 file changed · +67 −5
litellm/proxy/management_endpoints/key_management_endpoints.py+67 −5 modified@@ -456,23 +456,29 @@ def handle_key_type(data: GenerateKeyRequest, data_json: dict) -> dict: return data_json +_NON_ADMIN_SAFE_ALLOWED_ROUTES_PRESETS = frozenset({"llm_api_routes", "info_routes"}) + + def _check_allowed_routes_caller_permission( allowed_routes: Optional[list], user_api_key_dict: UserAPIKeyAuth, ) -> None: """ - Only proxy admins may set `allowed_routes` on a key. + Only proxy admins may set `allowed_routes` on a key, except for the safe + presets produced by `handle_key_type` for non-elevated buckets + (`llm_api_routes`, `info_routes`). - `allowed_routes` bypasses the standard role-based route gate in - RouteChecks.non_proxy_admin_allowed_routes_check, so if a non-admin is - allowed to set it they can grant themselves access to any endpoint. - Non-admins should use `key_type` to pick a preset route bucket instead. + `allowed_routes` overrides the standard role-based route gate in + RouteChecks.non_proxy_admin_allowed_routes_check, so the field is + restricted to admins outside of those safe presets. """ # Empty list is the default on GenerateKeyRequest — treat as "not set". if not allowed_routes: return if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value: return + if all(r in _NON_ADMIN_SAFE_ALLOWED_ROUTES_PRESETS for r in allowed_routes): + return raise HTTPException( status_code=403, detail={ @@ -484,6 +490,36 @@ def _check_allowed_routes_caller_permission( ) +def _check_passthrough_routes_caller_permission( + data: BaseModel, + user_api_key_dict: UserAPIKeyAuth, +) -> None: + """ + Only proxy admins may set `allowed_passthrough_routes` on a key, either at + the top level of the request or nested under `metadata`. + + The route gate evaluates passthrough access ahead of the standard role + gate, so the field is restricted to admins to keep that ordering safe. + """ + if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value: + return + if getattr(data, "allowed_passthrough_routes", None): + raise HTTPException( + status_code=403, + detail={ + "error": "Only proxy admins can set `allowed_passthrough_routes` on a key." + }, + ) + metadata = getattr(data, "metadata", None) + if isinstance(metadata, dict) and metadata.get("allowed_passthrough_routes"): + raise HTTPException( + status_code=403, + detail={ + "error": "Only proxy admins can set `metadata.allowed_passthrough_routes` on a key." + }, + ) + + async def validate_team_id_used_in_service_account_request( team_id: Optional[str], prisma_client: Optional[PrismaClient], @@ -674,6 +710,14 @@ async def _common_key_generation_helper( # noqa: PLR0915 data_json = handle_key_type(data, data_json) + # Re-check allowed_routes after handle_key_type, since key_type can derive + # an elevated bucket (e.g. ["management_routes"]) that wasn't present in + # the original request body. + _check_allowed_routes_caller_permission( + allowed_routes=data_json.get("allowed_routes"), + user_api_key_dict=user_api_key_dict, + ) + # if we get max_budget passed to /key/generate, then use it as key_max_budget. Since generate_key_helper_fn is used to make new users if "max_budget" in data_json: data_json["key_max_budget"] = data_json.pop("max_budget", None) @@ -1288,6 +1332,10 @@ async def generate_key_fn( allowed_routes=data.allowed_routes, user_api_key_dict=user_api_key_dict, ) + _check_passthrough_routes_caller_permission( + data=data, + user_api_key_dict=user_api_key_dict, + ) # For non-admin internal users: auto-assign caller's user_id if not provided # This prevents creating unbound keys with no user association (LIT-1884) @@ -1940,6 +1988,10 @@ async def _validate_update_key_data( allowed_routes=data.allowed_routes, user_api_key_dict=user_api_key_dict, ) + _check_passthrough_routes_caller_permission( + data=data, + user_api_key_dict=user_api_key_dict, + ) # Prevent non-admin from removing user_id (setting to empty string) (LIT-1884) if data.user_id is not None and data.user_id == "" and not _is_proxy_admin: @@ -3889,6 +3941,16 @@ async def regenerate_key_fn( # noqa: PLR0915 user_api_key_cache, ) + if data is not None: + _check_allowed_routes_caller_permission( + allowed_routes=data.allowed_routes, + user_api_key_dict=user_api_key_dict, + ) + _check_passthrough_routes_caller_permission( + data=data, + user_api_key_dict=user_api_key_dict, + ) + is_master_key_regeneration = data and data.new_master_key is not None if (
d910a95661fcfix(proxy): improve input validation on management endpoints
4 files changed · +258 −21
litellm/integrations/dotprompt/prompt_manager.py+6 −2 modified@@ -7,7 +7,8 @@ from typing import Any, Dict, List, Optional, Tuple, Union import yaml -from jinja2 import DictLoader, Environment, select_autoescape +from jinja2 import DictLoader, select_autoescape +from jinja2.sandbox import ImmutableSandboxedEnvironment class PromptTemplate: @@ -59,7 +60,10 @@ def __init__( self.prompt_directory = Path(prompt_directory) if prompt_directory else None self.prompts: Dict[str, PromptTemplate] = {} self.prompt_file = prompt_file - self.jinja_env = Environment( + # Sandboxed env: templates can come from user input via /prompts/test, + # so we must block access to unsafe Python attributes and mutation of + # caller-supplied mutables. + self.jinja_env = ImmutableSandboxedEnvironment( loader=DictLoader({}), autoescape=select_autoescape(["html", "xml"]), # Use Handlebars-style delimiters to match Dotprompt spec
litellm/proxy/management_endpoints/key_management_endpoints.py+58 −19 modified@@ -456,6 +456,34 @@ def handle_key_type(data: GenerateKeyRequest, data_json: dict) -> dict: return data_json +def _check_allowed_routes_caller_permission( + allowed_routes: Optional[list], + user_api_key_dict: UserAPIKeyAuth, +) -> None: + """ + Only proxy admins may set `allowed_routes` on a key. + + `allowed_routes` bypasses the standard role-based route gate in + RouteChecks.non_proxy_admin_allowed_routes_check, so if a non-admin is + allowed to set it they can grant themselves access to any endpoint. + Non-admins should use `key_type` to pick a preset route bucket instead. + """ + # Empty list is the default on GenerateKeyRequest — treat as "not set". + if not allowed_routes: + return + if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value: + return + raise HTTPException( + status_code=403, + detail={ + "error": ( + "Only proxy admins can set `allowed_routes` on a key. " + "Use `key_type` to pick a preset route bucket instead." + ) + }, + ) + + async def validate_team_id_used_in_service_account_request( team_id: Optional[str], prisma_client: Optional[PrismaClient], @@ -740,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) @@ -1254,6 +1282,12 @@ async def generate_key_fn( raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=message ) + + _check_allowed_routes_caller_permission( + allowed_routes=data.allowed_routes, + user_api_key_dict=user_api_key_dict, + ) + # For non-admin internal users: auto-assign caller's user_id if not provided # This prevents creating unbound keys with no user association (LIT-1884) _is_proxy_admin = ( @@ -1888,6 +1922,11 @@ async def _validate_update_key_data( """Validate permissions and constraints for key update.""" _is_proxy_admin = user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value + _check_allowed_routes_caller_permission( + allowed_routes=data.allowed_routes, + user_api_key_dict=user_api_key_dict, + ) + # Prevent non-admin from removing user_id (setting to empty string) (LIT-1884) if data.user_id is not None and data.user_id == "" and not _is_proxy_admin: raise HTTPException( @@ -3233,10 +3272,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: @@ -3436,9 +3475,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 @@ -4078,11 +4117,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: @@ -4165,10 +4204,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 []
litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py+27 −0 modified@@ -1,6 +1,7 @@ #### CRUD ENDPOINTS for UI Settings ##### import json from typing import Any, Dict, List, Optional, Union +from urllib.parse import urlparse from fastapi import APIRouter, Depends, File, HTTPException, UploadFile @@ -817,6 +818,29 @@ async def get_ui_theme_settings(): ) +def _validate_public_image_url(value: Optional[str], field_name: str) -> None: + """ + Reject anything that isn't a plain http(s) URL with a host. This value is + later served via the unauthenticated /get_image endpoint, so local paths + like "/etc/passwd" or "file://..." must not be accepted. + """ + if value is None: + return + if not isinstance(value, str) or not value.strip(): + return + parsed = urlparse(value.strip()) + if parsed.scheme not in ("http", "https") or not parsed.netloc: + raise HTTPException( + status_code=400, + detail={ + "error": ( + f"Invalid {field_name}: must be an http(s) URL with a host. " + "Local filesystem paths and non-http schemes are not allowed." + ) + }, + ) + + @router.patch( "/update/ui_theme_settings", tags=["UI Theme Settings"], @@ -831,6 +855,9 @@ async def update_ui_theme_settings(theme_config: UIThemeConfig): from litellm.proxy.proxy_server import proxy_config, store_model_in_db + _validate_public_image_url(theme_config.logo_url, "logo_url") + _validate_public_image_url(theme_config.favicon_url, "favicon_url") + if store_model_in_db is not True: raise HTTPException( status_code=500,
tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py+167 −0 modified@@ -8579,3 +8579,170 @@ def test_enforce_upperbound_no_config_is_noop(): assert data.tpm_limit == 999999 finally: litellm.upperbound_key_generate_params = original + + +class TestAllowedRoutesCallerPermission: + """ + Non-admins must not be able to set `allowed_routes` on a key. The field + bypasses the role-based route gate in + RouteChecks.non_proxy_admin_allowed_routes_check, so allowing a non-admin + to populate it grants them arbitrary endpoint access. + """ + + @pytest.mark.asyncio + async def test_non_admin_generate_key_with_allowed_routes_rejected(self): + data = GenerateKeyRequest( + key_alias="escalate", + allowed_routes=["/*"], + ) + user_api_key_dict = UserAPIKeyAuth( + user_id="internal-user-123", + user_role=LitellmUserRoles.INTERNAL_USER, + ) + mock_prisma_client = AsyncMock() + + with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client), patch( + "litellm.proxy.proxy_server.user_api_key_cache", MagicMock() + ), patch("litellm.proxy.proxy_server.user_custom_key_generate", None), patch( + "litellm.proxy.management_endpoints.key_management_endpoints._common_key_generation_helper", + new_callable=AsyncMock, + return_value=MagicMock(), + ): + with pytest.raises(ProxyException) as exc_info: + await generate_key_fn( + data=data, + user_api_key_dict=user_api_key_dict, + litellm_changed_by=None, + ) + assert str(exc_info.value.code) == "403" + assert "allowed_routes" in str(exc_info.value.message) + + @pytest.mark.asyncio + async def test_admin_generate_key_with_allowed_routes_allowed(self): + data = GenerateKeyRequest( + key_alias="admin-key", + allowed_routes=["/chat/completions"], + user_id="admin-user", + ) + user_api_key_dict = UserAPIKeyAuth( + user_id="admin-user", + user_role=LitellmUserRoles.PROXY_ADMIN, + ) + mock_prisma_client = AsyncMock() + stub_response = MagicMock() + + with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client), patch( + "litellm.proxy.proxy_server.user_api_key_cache", MagicMock() + ), patch("litellm.proxy.proxy_server.user_custom_key_generate", None), patch( + "litellm.proxy.management_endpoints.key_management_endpoints._common_key_generation_helper", + new_callable=AsyncMock, + return_value=stub_response, + ): + result = await generate_key_fn( + data=data, + user_api_key_dict=user_api_key_dict, + litellm_changed_by=None, + ) + assert result is stub_response + + @pytest.mark.asyncio + async def test_non_admin_generate_key_default_empty_allowed_routes_ok(self): + """ + Regression guard: GenerateKeyRequest.allowed_routes defaults to [], so + the helper must treat empty-list as "not set" or every non-admin key + creation breaks. + """ + data = GenerateKeyRequest(key_alias="plain-key") + user_api_key_dict = UserAPIKeyAuth( + user_id="internal-user-123", + user_role=LitellmUserRoles.INTERNAL_USER, + ) + mock_prisma_client = AsyncMock() + stub_response = MagicMock() + + with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client), patch( + "litellm.proxy.proxy_server.user_api_key_cache", MagicMock() + ), patch("litellm.proxy.proxy_server.user_custom_key_generate", None), patch( + "litellm.proxy.management_endpoints.key_management_endpoints._common_key_generation_helper", + new_callable=AsyncMock, + return_value=stub_response, + ): + result = await generate_key_fn( + data=data, + user_api_key_dict=user_api_key_dict, + litellm_changed_by=None, + ) + assert result is stub_response + + @pytest.mark.asyncio + async def test_non_admin_update_key_with_allowed_routes_rejected(self): + from litellm.proxy.management_endpoints.key_management_endpoints import ( + update_key_fn, + ) + + data = UpdateKeyRequest(key="sk-test", allowed_routes=["/*"]) + user_api_key_dict = UserAPIKeyAuth( + user_id="internal-user-123", + user_role=LitellmUserRoles.INTERNAL_USER, + ) + mock_prisma_client = AsyncMock() + + with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client), patch( + "litellm.proxy.proxy_server.user_api_key_cache", MagicMock() + ), patch("litellm.proxy.proxy_server.user_custom_key_update", None), patch( + "litellm.proxy.proxy_server.llm_router", None + ), patch("litellm.proxy.proxy_server.premium_user", True), patch( + "litellm.proxy.proxy_server.proxy_logging_obj", MagicMock() + ), patch( + "litellm.proxy.management_endpoints.key_management_endpoints._get_and_validate_existing_key", + new_callable=AsyncMock, + return_value=MagicMock(), + ): + with pytest.raises(ProxyException) as exc_info: + await update_key_fn( + request=MagicMock(), + data=data, + user_api_key_dict=user_api_key_dict, + litellm_changed_by=None, + ) + assert str(exc_info.value.code) == "403" + assert "allowed_routes" in str(exc_info.value.message) + + +def test_jinja_prompt_manager_is_sandboxed(): + """ + PromptManager renders user-supplied templates via /prompts/test, so its + jinja env must reject access to unsafe Python attributes like + ``__class__`` and ``__mro__``. + """ + from jinja2.exceptions import SecurityError + + from litellm.integrations.dotprompt.prompt_manager import PromptManager + + pm = PromptManager() + template = pm.jinja_env.from_string("{{ ''.__class__.__mro__ }}") + with pytest.raises(SecurityError): + template.render() + + +def test_validate_public_image_url_rejects_local_paths(): + from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import ( + _validate_public_image_url, + ) + + for bad in ("/etc/passwd", "file:///etc/passwd", "../../etc/passwd"): + with pytest.raises(HTTPException) as exc_info: + _validate_public_image_url(bad, "logo_url") + assert exc_info.value.status_code == 400 + + +def test_validate_public_image_url_accepts_http_and_noop_empty(): + from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import ( + _validate_public_image_url, + ) + + _validate_public_image_url("https://example.com/logo.png", "logo_url") + _validate_public_image_url("http://cdn.internal/logo.svg", "logo_url") + _validate_public_image_url(None, "logo_url") + _validate_public_image_url("", "logo_url") + _validate_public_image_url(" ", "logo_url")
Vulnerability mechanics
Root cause
"Missing authorization check on the `allowed_routes` field when creating or updating API keys allows an authenticated internal_user to specify routes that exceed their role's permissions."
Attack vector
An authenticated user with the `internal_user` role sends a request to `/key/generate`, `/key/update`, `/key/regenerate`, or the service-account key endpoint with an `allowed_routes` field containing routes reserved for `proxy_admin` (e.g. `["/*"]` or `["management_routes"]`). The server stores the key with those routes without verifying that the caller's role permits them. The attacker then uses the newly created key to make requests to the admin-only endpoints; because `allowed_routes` overrides the standard role-based route gate in `RouteChecks.non_proxy_admin_allowed_routes_check`, the requests succeed, achieving full privilege escalation to `proxy_admin` [patch_id=1264296][patch_id=1264297][patch_id=1264298].
Affected code
The vulnerability exists in `litellm/proxy/management_endpoints/key_management_endpoints.py` within the `generate_key_fn`, `_validate_update_key_data`, `regenerate_key_fn`, and `generate_service_account_key_fn` functions. These endpoints accepted the `allowed_routes` field from non-admin users without verifying the caller's role permissions. The `_common_key_generation_helper` function also lacked a re-check after `handle_key_type` could derive elevated route buckets [patch_id=1264296][patch_id=1264297].
What the fix does
The patches introduce `_check_allowed_routes_caller_permission()` and `_check_passthrough_routes_caller_permission()` which reject non-admin requests that include `allowed_routes` or `allowed_passthrough_routes` [patch_id=1264296]. These checks are inserted into `generate_key_fn`, `_validate_update_key_data`, `regenerate_key_fn`, and `generate_service_account_key_fn` [patch_id=1264296][patch_id=1264297]. A second patch refines the logic so that non-admins may still use the `key_type` parameter to derive safe preset buckets (`llm_api_routes`, `info_routes`) via `handle_key_type`, but raw-body `allowed_routes` values remain blocked [patch_id=1264297]. The third patch adds comprehensive unit tests confirming that non-admin key generation with `allowed_routes` is rejected while admin usage and default empty lists continue to work [patch_id=1264298].
Preconditions
- authAttacker must have a valid authenticated session with the internal_user role
- networkAttacker must be able to reach the /key/generate, /key/update, /key/regenerate, or service-account key management endpoints over the network
- configThe LiteLLM proxy must be running a version prior to 1.83.14
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/2220f3076ac89bd2a2e3439acf57dcfbec2434c9nvd
- github.com/BerriAI/litellm/commit/5190bd07eb23a037745d86328096f54378f1614anvd
- github.com/BerriAI/litellm/commit/d910a95661fce3cdd36f3b06c03ecf9c46c6457cnvd
- github.com/BerriAI/litellm/releases/tag/v1.83.14-stablenvd
- huntr.com/bounties/8e75edfb-ff05-4e63-bfca-2d93d03fb3b9nvd
- www.vulncheck.com/advisories/litellm-privilege-escalation-via-api-key-generationnvd
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