# [CRIT] [GHSA / CRITICAL] CVE-2026-49468: LiteLLM: Authentication Bypass via Host Header Injection

**Source:** GitHub Security Advisories
**Published:** 2026-06-16
**Article:** https://github.com/advisories/GHSA-4xpc-pv4p-pm3w

## Threat Profile

LiteLLM: Authentication Bypass via Host Header Injection

### Impact

A Host-header parsing flaw in the LiteLLM proxy could, under specific conditions, allow unauthenticated access to protected management routes.

The auth layer derived the effective route from `request.url.path` in `litellm/proxy/auth/auth_utils.py::get_request_route()`, which Starlette reconstructs from the `Host` header. A crafted `Host` could therefore make the auth gate evaluate a different route from the one FastAPI dispat…

## Indicators of Compromise (high-fidelity only)

- **CVE:** `CVE-2026-49468`

## MITRE ATT&CK Techniques

- **T1190** — Exploit Public-Facing Application
- **T1036** — Masquerading
- **T1078** — Valid Accounts
- **T1046** — Network Service Discovery
- **T1595.002** — Active Scanning: Vulnerability Scanning

## Kill chain phases observed

_(none detected from narrative keywords)_

## Recommended hunts

### Vulnerable LiteLLM proxy (< 1.84.0) installed — CVE-2026-49468 exposure

`UC_21_1` · phase: **weapon** · confidence: **High** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count, min(_time) as firstSeen, max(_time) as lastSeen, values(Vulnerabilities.signature) as signatures from datamodel=Vulnerabilities where (Vulnerabilities.cve="CVE-2026-49468" OR (Vulnerabilities.signature="*litellm*" AND Vulnerabilities.severity IN ("high","critical"))) by Vulnerabilities.dest, Vulnerabilities.signature | `drop_dm_object_name(Vulnerabilities)` | convert ctime(firstSeen) ctime(lastSeen)
```

**Defender KQL:**
```kql
DeviceTvmSoftwareVulnerabilities
| where CveId == "CVE-2026-49468" or (SoftwareName has "litellm" and SoftwareVendor has_any ("berriai","litellm","python"))
| where SoftwareVersion !startswith "1.84" and SoftwareVersion !startswith "1.85" and SoftwareVersion !startswith "1.86" and SoftwareVersion !startswith "1.9" and SoftwareVersion !startswith "2."
| join kind=leftouter (DeviceInfo | summarize arg_max(Timestamp, IsInternetFacing, PublicIP) by DeviceId) on DeviceId
| project Timestamp, DeviceName, DeviceId, SoftwareName, SoftwareVersion, CveId, VulnerabilitySeverityLevel, RecommendedSecurityUpdate, IsInternetFacing, PublicIP
| order by IsInternetFacing desc, Timestamp desc
```

### LiteLLM admin-route request with suspicious / mismatched Host header (CVE-2026-49468)

`UC_21_2` · phase: **exploit** · confidence: **Medium** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count, values(Web.http_method) as methods, values(Web.status) as statuses, values(Web.url) as urls, values(Web.dest) as dests from datamodel=Web where (Web.uri_path="/key/*" OR Web.uri_path="/user/*" OR Web.uri_path="/team/*" OR Web.uri_path="/model/*" OR Web.uri_path="/customer/*" OR Web.uri_path="/spend/*" OR Web.uri_path="/organization/*") by Web.src, Web.dest, host, _time span=5m | `drop_dm_object_name(Web)` | join type=left host [search index=* sourcetype IN ("nginx:plus:kv","nginx:plus:access","access_combined","ms:iis:auto","aws:elasticloadbalancing:accesslogs") (uri_path="/key/*" OR uri_path="/user/*" OR uri_path="/team/*" OR uri_path="/model/*" OR uri_path="/customer/*" OR uri_path="/spend/*" OR uri_path="/organization/*") | rex field=_raw "(?i)host:\s*(?<host_hdr>[^\s;]+)" | eval host_anomaly=case(match(host_hdr,"(?i)^\d{1,3}(\.\d{1,3}){3}"),"ip_literal", match(host_hdr,"[;@]"),"injection_char", match(host_hdr,"\.\."),"path_traversal", match(host_hdr,"/"),"path_in_host", isnull(host_hdr) OR host_hdr="","empty_host", 1==1,"normal") | where host_anomaly!="normal" | stats values(host_hdr) as host_headers, values(host_anomaly) as anomalies by host] | where isnotnull(anomalies)
```

### LiteLLM admin endpoint 401→200 sequence from same source (auth bypass)

`UC_21_3` · phase: **exploit** · confidence: **Medium** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count, values(Web.uri_path) as paths, values(Web.status) as statuses, min(_time) as firstSeen, max(_time) as lastSeen from datamodel=Web where (Web.uri_path="/key/*" OR Web.uri_path="/user/*" OR Web.uri_path="/team/*" OR Web.uri_path="/model/*" OR Web.uri_path="/customer/*" OR Web.uri_path="/spend/*" OR Web.uri_path="/organization/*") by Web.src, Web.dest, _time span=5m | `drop_dm_object_name(Web)` | eval failed=if(match(mvjoin(statuses,","), "401|403"),1,0), succeeded=if(match(mvjoin(statuses,","),"200|201|204"),1,0) | where failed=1 AND succeeded=1 AND mvcount(paths)>=2 | convert ctime(firstSeen) ctime(lastSeen)
```

### LiteLLM rapid admin-route enumeration across distinct management categories

`UC_21_4` · phase: **actions** · confidence: **Medium** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count, values(Web.uri_path) as paths, values(Web.status) as statuses, min(_time) as firstSeen, max(_time) as lastSeen from datamodel=Web where Web.status IN (200,201,204) AND (Web.uri_path="/key/*" OR Web.uri_path="/user/*" OR Web.uri_path="/team/*" OR Web.uri_path="/model/*" OR Web.uri_path="/customer/*" OR Web.uri_path="/spend/*" OR Web.uri_path="/organization/*") by Web.src, Web.dest, _time span=10m | `drop_dm_object_name(Web)` | eval categories=mvmap(paths, case(match('paths',"^/key/"),"key", match('paths',"^/user/"),"user", match('paths',"^/team/"),"team", match('paths',"^/model/"),"model", match('paths',"^/customer/"),"customer", match('paths',"^/spend/"),"spend", match('paths',"^/organization/"),"org")) | eval distinct_categories=mvcount(mvdedup(categories)) | where distinct_categories >= 3 | convert ctime(firstSeen) ctime(lastSeen)
```

### Unauthenticated success on LiteLLM admin endpoint (Authorization header absent)

`UC_21_5` · phase: **exploit** · confidence: **High** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count, values(Web.http_method) as methods, values(Web.url) as urls, values(Web.http_user_agent) as user_agents from datamodel=Web where Web.status IN (200,201,204) AND (Web.uri_path="/key/*" OR Web.uri_path="/user/*" OR Web.uri_path="/team/*" OR Web.uri_path="/model/*" OR Web.uri_path="/customer/*" OR Web.uri_path="/spend/*" OR Web.uri_path="/organization/*") by Web.src, Web.dest, host, _time span=1h | `drop_dm_object_name(Web)` | join type=inner host [search index=* sourcetype IN ("nginx:plus:kv","nginx:plus:access","access_combined","ms:iis:auto","aws:elasticloadbalancing:accesslogs") status IN (200,201,204) (uri_path="/key/*" OR uri_path="/user/*" OR uri_path="/team/*" OR uri_path="/model/*" OR uri_path="/customer/*" OR uri_path="/spend/*" OR uri_path="/organization/*") | rex field=_raw "(?i)authorization:\s*(?<auth_hdr>\S+)" | rex field=_raw "(?i)(?:x-(?:api|litellm)-key|api[-_]?key):\s*(?<key_hdr>\S+)" | rex field=uri_query "(?i)(?:api_key|key)=(?<key_qs>[^&\s]+)" | eval no_auth=if(isnull(auth_hdr) AND isnull(key_hdr) AND isnull(key_qs),1,0) | where no_auth=1 | stats count by host, uri_path, src, _time]
```

### LiteLLM proxy reconnaissance — /health /version /docs probes on default ports

`UC_21_6` · phase: **recon** · confidence: **Medium** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count, values(Web.uri_path) as paths, values(Web.http_user_agent) as user_agents, dc(Web.uri_path) as distinct_paths from datamodel=Web where Web.dest_port IN (4000,8000,8001) AND (Web.uri_path="/health*" OR Web.uri_path="/version" OR Web.uri_path="/docs*" OR Web.uri_path="/openapi.json" OR Web.uri_path="/sso/key/generate" OR Web.uri_path="/redoc") by Web.src, Web.dest, _time span=10m | `drop_dm_object_name(Web)` | eval scanner_ua=if(match(mvjoin(user_agents,"|"), "(?i)(nmap|nuclei|masscan|zgrab|python-requests|curl/|go-http-client|gobuster|ffuf)"),1,0) | eval external_src=if(NOT cidrmatch("10.0.0.0/8",src) AND NOT cidrmatch("172.16.0.0/12",src) AND NOT cidrmatch("192.168.0.0/16",src),1,0) | where external_src=1 OR (scanner_ua=1 AND distinct_paths>=2)
```

**Defender KQL:**
```kql
DeviceNetworkEvents
| where Timestamp > ago(7d)
| where RemotePort in (4000, 8000, 8001)
| where ActionType == "InboundConnectionAccepted"
| where RemoteIPType == "Public"
| join kind=inner (
    DeviceProcessEvents
    | where Timestamp > ago(7d)
    | where InitiatingProcessFileName has_any ("python.exe","python3","uvicorn","gunicorn")
    | where InitiatingProcessCommandLine has "litellm"
    | distinct DeviceId
  ) on DeviceId
| summarize ConnectionCount = count(), DistinctRemoteIPs = dcount(RemoteIP), RemoteIPs = make_set(RemoteIP, 25) by DeviceName, LocalPort, bin(Timestamp, 1h)
| where DistinctRemoteIPs >= 5 or ConnectionCount >= 20
| order by DistinctRemoteIPs desc
```

### IOC-driven hunts (use shared templates)

These are standard IOC-substitution hunts — the canonical SPL and KQL live once in [`_TEMPLATES.md`](../_TEMPLATES.md), so we don't repeat the same boilerplate on every CVE / hash / network-IOC briefing.

- **Asset exposure — vulnerability matches article CVE(s)** ([template](../_TEMPLATES.md#asset-exposure)) — phase: **recon**, confidence: **High**
  - CVE(s): `CVE-2026-49468`


## Why this matters

Severity classified as **CRIT** based on: CVE present, 7 use case(s) fired, 5 technique(s) inferred. Read the full article for actor attribution, tooling details, and any defanged IOCs in the body that aren't visible in the RSS summary.
