I gateway LLM sono sistemi che si collocano tra un'applicazione e uno o più fornitori di LLM. Invece di chiamare direttamente le API dei modelli, le applicazioni instradano le richieste attraverso un gateway, che poi le inoltra al backend del modello appropriato.
In pratica, i gateway LLM vengono spesso utilizzati per centralizzare aspetti come l'astrazione del fornitore, la gestione delle credenziali, l'instradamento delle richieste, il rate limiting, il logging e il tracciamento dell'uso. Molti gateway aggiungono anche funzionalità di livello superiore come la logica di fallback, il bilanciamento del carico tra i fornitori e la selezione del modello basata sulla configurazione.
LiteLLM è un progetto open-source che fornisce un SDK e un prodotto gateway per lavorare con più fornitori di LLM. Il progetto è nato attraverso Y Combinator ed è ampiamente utilizzato nell'ecosistema LLM.
In questo articolo ci concentriamo esclusivamente sul gateway LiteLLM. Il gateway viene comunemente distribuito come servizio autonomo e svolge un ruolo centrale nel modo in cui le richieste LLM vengono gestite in molte applicazioni basate su LLM. La sua ampia adozione e la sua posizione nel percorso delle richieste lo hanno reso un bersaglio adatto per la nostra ricerca.
La nostra revisione di LiteLLM è stata condotta sulla versione 1.77.7. Le sezioni seguenti delineano i difetti che abbiamo scoperto nel gateway LiteLLM.
Vulnerabilità nell'endpoint /config/update
Il proxy LiteLLM espone un endpoint amministrativo 'POST /config/update', progettato per consentire modifiche alla configurazione del proxy in tempo reale. Questo endpoint facilita l'aggiornamento di impostazioni critiche, tra cui variabili d'ambiente, liste di modelli e impostazioni generali del proxy.
La vulnerabilità deriva da un controllo di autorizzazione basato su ruoli (RBAC) mancante all'interno dell'implementazione dell'endpoint, combinato con una lista di permessi per le rotte UI eccessivamente permissiva.
In endpoint amministrativi sicuri all'interno di LiteLLM (ad esempio, /invitation/new), il codice verifica esplicitamente che l'utente autenticato possieda il ruolo PROXY_ADMIN prima di procedere.
Tuttavia, la funzione updateconfig si affida interamente alla dipendenza userapikeyauth generica senza eseguire un controllo successivo sul ruolo dell'oggetto utente.
Codice vulnerabile
Il codice vulnerabile è il seguente:
@router.post("/config/update", dependencies=[Depends(userapikeyauth)], # Solo controllo di autenticazione generico includeinschema=False,) async def updateconfig(configinfo: ConfigYAML): # ... # MANCANTE: if userrole != PROXYADMIN: raise ... # ... # Procedere con l'aggiornamento della configurazione await proxyconfig.saveconfig(newconfig=config)
Autorizzazione basata su UI
La dipendenza userapikeyauth delega l'autorizzazione a una funzione commonchecks. Questa funzione determina se una richiesta è consentita in base al ruolo dell'utente e alla rotta di destinazione.
Quando qualsiasi utente effettua il login nell'interfaccia utente di LiteLLM, il backend genera una chiave API temporanea esplicitamente associata al team speciale litellm-dashboard. Questo avviene in diversi flussi di login:
response = await generatekeyhelperfn(requesttype="key", **{"userrole": userrole, "duration": "24hr", "keymaxbudget": litellm.maxuisessionbudget, "models": [], "aliases": {}, "config": {}, "spend": 0, "userid": userid, "teamid": "litellm-dashboard", # <– Assegnazione della chiave,})
Durante l'autorizzazione, la classe commonchecks classifica i token come "ui" o "api" in base esclusivamente al campo teamid:
tokenteam = getattr(validtoken, "teamid", None) tokentype: Literal["ui", "api"] = ("ui" if tokenteam is not None and tokenteam == "litellm-dashboard" else "api")
Se il token è classificato come "ui", la logica di autorizzazione passa a isuiroute() invece dei controlli più rigorosi di isapiroute_allowed():
def isallowedroute(route: str, tokentype: Literal["ui", "api"], # ...) -> bool: if tokentype == "ui" and isuiroute(route=route, userobj=userobj): return True # <– Bypassa i controlli delle rotte API più rigorosi else: return isapirouteallowed(...)
L'elenco ui_routes include /config, probabilmente inteso per consentire l'accesso in sola lettura alla configurazione:
uiroutes = ["/sso", "/sso/get/uisettings", "/login", "/key/info", "/config", # <– Probabilmente inteso per l'accesso in sola lettura alla configurazione "/spend", # ...]
Poiché /config/update soddisfa startswith("/config"), il controllo passa. La richiesta è consentita per qualsiasi utente con un token di sessione UI valido, indipendentemente dal loro ruolo effettivo (Amministratore, Utente Interno, ecc.).
Impatto della vulnerabilità
Questo fallimento del controllo di accesso consente a qualsiasi utente autenticato, inclusi quelli con il ruolo base "Utente Interno", di invocare POST /config/update e modificare la configurazione del proxy in tempo reale. Attraverso questo singolo endpoint esposto, gli attaccanti possono ottenere:
- Lettura di qualsiasi file accessibile al processo LiteLLM, inclusi file di configurazione, segreti e credenziali.
- Presa di controllo dell'account amministrativo.
- Esecuzione remota di codice tramite moduli Python dannosi caricati da archiviazione cloud.
Ogni scenario di sfruttamento è dettagliato nelle sezioni seguenti.
Lettura di file arbitrari
L'endpoint /config/update accetta un campo environmentvariables, che consente ai chiamanti di iniettare coppie chiave-valore arbitrarie nell'ambiente del proxy. Una di queste variabili, UILOGOPATH, controlla il percorso del file servito dall'endpoint /getimage. Impostando questa variabile su un percorso di file sensibile, un attaccante può leggere file arbitrari dal server.
Quando /config/update riceve variabili d'ambiente, le crittografa e le salva nello store di configurazione. Questo risulta nell'esecuzione del seguente codice, che decrittografa questi valori e li scrive direttamente in os.environ:
def decryptandsetdbenvvariables(self, environment_variables: dict) -> dict: