VAITP Dataset

← Back to the dataset

CVE-2026-35029

LiteLLM lacks authorization on /config/update, allowing authenticated RCE.

  • CVSS 8.7
  • CWE-863
  • Authentication, Authorization, and Session Management
  • Remote

LiteLLM is a proxy server (AI Gateway) to call LLM APIs in OpenAI (or native) format. Prior to 1.83.0, the /config/update endpoint does not enforce admin role authorization. A user who is already authenticated into the platform can then use this endpoint to modify proxy configuration and environment variables, register custom pass-through endpoint handlers pointing to attacker-controlled Python code, achieving remote code execution, read arbitrary server files by setting UI_LOGO_PATH and fetching via /get_image, and take over other privileged accounts by overwriting UI_USERNAME and UI_PASSWORD environment variables. Fixed in v1.83.0.

CVSS base score
8.7
Published
2026-04-06
OWASP
A01 Broken Access Control
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Authentication, Authorization, and Session Management
Subcategory
Privilege Escalation
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
LiteLLM
Fixed by upgrading
Yes

Solution

Upgrade to LiteLLM version 1.83.0 or later.

Vulnerable code sample

import os
import importlib
from fastapi import FastAPI, Request, Depends, HTTPException, status
from pydantic import BaseModel, Field
from typing import Dict, Optional, Any

# --- Mocks and Simulated Environment for Demonstration ---

class User:
    """A mock user model."""
    def __init__(self, username: str, role: str):
        self.username = username
        self.role = role

# A hardcoded "database" of users and tokens to simulate an auth system.
FAKE_USERS_DB = {
    "user_token": User(username="normal_user", role="user"),
    "admin_token": User(username="admin_user", role="admin"),
}

def get_current_user(request: Request) -> User:
    """
    Simulates a dependency that gets the current authenticated user from a token.
    In the vulnerability, a check for an 'admin' role is missing in the endpoint
    that uses this dependency.
    """
    token = request.headers.get("Authorization")
    if token and token in FAKE_USERS_DB:
        return FAKE_USERS_DB[token]
    raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Invalid or missing authentication token",
    )

# --- FastAPI Application ---

app = FastAPI()

# A global variable to simulate LiteLLM's in-memory configuration.
LITELLM_CONFIG: Dict[str, Any] = {
    "model_list": [
        {"model_name": "gpt-3.5-turbo", "litellm_params": {"model": "azure/gpt-3.5-turbo"}}
    ],
    "litellm_settings": {
        "set_verbose": False,
    },
}

# --- Pydantic Models for the API Endpoint ---

class ConfigUpdate(BaseModel):
    """
    Defines the payload for the /config/update endpoint, allowing modification
    of critical server settings.
    """
    environment_variables: Optional[Dict[str, str]] = None
    litellm_settings: Optional[Dict[str, Any]] = None
    model_list: Optional[list] = None
    custom_code_module: Optional[str] = Field(None, description="Module to import for custom logic (RCE vector)")

# --- Vulnerable Endpoint Definition (Pre-v1.83.0) ---

@app.post("/config/update")
async def update_config(
    config_update: ConfigUpdate,
    current_user: User = Depends(get_current_user)  # Step 1: User is authenticated.
):
    """
    This endpoint updates the server's configuration. It authenticates the user
    but fails to perform an authorization check to ensure the user is an admin.
    This allows any authenticated user to modify the server's behavior.
    """
    # Step 2: The authorization check is missing.
    # A correct implementation would have a check here like:
    # if current_user.role != "admin":
    #     raise HTTPException(status_code=403, detail="Not authorized")

    # Vector 1: Arbitrary Environment Variable Overwrite.
    # An attacker can set variables like 'UI_USERNAME', 'UI_PASSWORD' to take
    # over the admin account, or 'UI_LOGO_PATH' to read arbitrary files.
    if config_update.environment_variables:
        for key, value in config_update.environment_variables.items():
            os.environ[key] = str(value)

    # Vector 2: Proxy Configuration Modification.
    # An attacker can alter the model list or other settings.
    if config_update.model_list:
        LITELLM_CONFIG["model_list"] = config_update.model_list
    if config_update.litellm_settings:
        LITELLM_CONFIG["litellm_settings"].update(config_update.litellm_settings)

    # Vector 3: Remote Code Execution.
    # An attacker can specify a Python module path. If they can get a
    # malicious file onto the server, this will import and execute it.
    if config_update.custom_code_module:
        try:
            # Dynamically importing the module will execute its top-level code.
            importlib.import_module(config_update.custom_code_module)
        except ImportError as e:
            # The application might silently fail or log the error.
            print(f"Failed to import custom module: {e}")
            pass

    return {"status": "success", "message": "Configuration updated."}

@app.get("/config")
async def get_config(current_user: User = Depends(get_current_user)):
    """A helper endpoint to view the effects of the vulnerability."""
    return {
        "LITELLM_CONFIG": LITELLM_CONFIG,
        "UI_USERNAME": os.environ.get("UI_USERNAME"),
        "UI_PASSWORD": os.environ.get("UI_PASSWORD"),
        "UI_LOGO_PATH": os.environ.get("UI_LOGO_PATH"),
    }

Patched code sample

import os
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from typing import Optional, Dict, Any

# This is a mock representation of LiteLLM's internal configuration and authentication
# to make the example runnable and demonstrate the fix context.

class MockProxyConfig:
    def __init__(self):
        # In a real scenario, this dictionary holds user API keys and their roles.
        # An admin user has the role 'admin'. Other authenticated users might have 'user'.
        self.user_api_key_dict: Optional[Dict[str, Dict[str, Any]]] = {
            "sk-admin-key-1234": {"role": "admin"},
            "sk-user-key-5678": {"role": "user"},
        }

proxy_config = MockProxyConfig()

class UpdateProxyConfig(BaseModel):
    # Represents the data structure for updating the configuration.
    model_name: Optional[str] = None
    litellm_settings: Optional[Dict[str, Any]] = None
    litellm_params: Optional[Dict[str, Any]] = None

# A mock dependency to simulate extracting the user's API key from the request.
async def user_api_key_auth(request: Request) -> str:
    auth_header = request.headers.get("Authorization")
    if not auth_header or not auth_header.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Not authenticated")
    key = auth_header.split(" ")[1]
    # In a real scenario, this would also validate the key exists.
    if key not in proxy_config.user_api_key_dict:
        raise HTTPException(status_code=401, detail="Invalid API Key")
    return key

# FastAPI router for defining the endpoint.
router = APIRouter()


# The following function demonstrates the fix for CVE-2024-35029.
# The vulnerable version of this function was missing the role-based authorization check.
# The fix involves adding a check to ensure the user associated with the provided
# API key has the 'admin' role before allowing any configuration updates.

@router.post("/config/update")
async def update_proxy_config(
    data: UpdateProxyConfig,
    request: Request,
    user_api_key: str = Depends(user_api_key_auth),
):
    """
    Update the proxy configuration.
    This endpoint is now protected and requires admin privileges.
    """
    # ------------ FIX STARTS HERE ------------
    # Check if the user making the request has the 'admin' role.
    if proxy_config.user_api_key_dict is not None:
        # Get the role for the provided API key. Default to 'user' if not specified.
        user_role = proxy_config.user_api_key_dict.get(user_api_key, {}).get(
            "role", "user"
        )
        
        # If the user's role is not 'admin', deny access.
        if user_role != "admin":
            raise HTTPException(
                status_code=403, detail="Forbidden. User is not an admin."
            )
    # ------------- FIX ENDS HERE -------------

    # If the check passes, the original logic to update the configuration proceeds.
    # (The actual update logic is omitted for clarity as it is not part of the fix.)
    print(f"Admin user '{user_api_key}' is updating the configuration.")
    print(f"Received update data: {data.dict()}")

    return {"status": "success", "message": "Configuration update processed by admin."}

# To test this code:
# 1. Install necessary packages: pip install fastapi "uvicorn[standard]"
# 2. Run this script with: uvicorn <filename>:app --reload
# 3. Use a tool like `curl` to test the endpoint.

# Example of creating a minimal FastAPI app to host the router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)

# Example curl commands for testing:

# This call will FAIL (403 Forbidden) because the key belongs to a non-admin user.
# curl -X POST http://127.0.0.1:8000/config/update \
# -H "Content-Type: application/json" \
# -H "Authorization: Bearer sk-user-key-5678" \
# -d '{"litellm_params": {"new_setting": "value"}}'

# This call will SUCCEED (200 OK) because the key belongs to an admin user.
# curl -X POST http://127.0.0.1:8000/config/update \
# -H "Content-Type: application/json" \
# -H "Authorization: Bearer sk-admin-key-1234" \
# -d '{"litellm_params": {"new_setting": "value"}}'

Payload

{
  "model_list": [
    {
      "model_name": "rce-payload",
      "litellm_params": {
        "model": "python",
        "api_base": "import os\nos.system('touch /tmp/pwned')"
      }
    }
  ]
}

Cite this entry

@misc{vaitp:cve202635029,
  title        = {{LiteLLM lacks authorization on /config/update, allowing authenticated RCE.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-35029},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-35029/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::