mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-06 20:25:29 +00:00
fix(oauth2-proxy): drop premium gate; identity-only allowlist is the security fix
Greptile flagged the ``premium_user is not True`` check as a hard
backwards-incompatible break for OSS users currently running
``enable_oauth2_proxy_auth=True``. They were right: unlike the
api_base case (where the docs already required admin opt-in), this
path was documented as available to OSS users. Adding the gate would
have closed a documented feature, not fixed a vuln.
Reframed the change:
* The **identity-only allowlist** (``ALLOWED_OAUTH2_PROXY_FIELDS`` =
``{user_id, user_email, team_id, team_alias, org_id, models}``) is
the actual security fix — it closes the privesc by rejecting any
mapping to a non-identity field at request time. This is unchanged.
* The **premium gate** was parity-with-siblings (a product decision,
not a security one). Removed. BerriAI can re-add it on their own
schedule with a proper deprecation cycle if they want enterprise-
only gating.
Tests: removed ``test_rejects_when_not_premium``; everything else
(allowlist enforcement, identity passthrough, attack-shape
regression) still passes — 14 tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ from typing import Any, Dict, FrozenSet
|
||||
from fastapi import Request
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
|
||||
# OAuth2-proxy header trust is for **identity assertion** from a trusted
|
||||
# upstream auth proxy (oauth2-proxy, Authelia, etc.). The allowlist below
|
||||
@@ -42,31 +42,19 @@ async def handle_oauth2_proxy_request(request: Request) -> UserAPIKeyAuth:
|
||||
|
||||
The auth model assumes the proxy is deployed behind a trusted OAuth2
|
||||
reverse proxy that injects authenticated identity headers (e.g.
|
||||
oauth2-proxy, Authelia). Two safeguards above and beyond that
|
||||
deployment assumption:
|
||||
oauth2-proxy, Authelia).
|
||||
|
||||
1. **Premium gate.** The sibling auth paths (``enable_oauth2_auth``
|
||||
and ``enable_jwt_auth``) require ``premium_user``; this path
|
||||
previously did not, which let any open-source deployment turn
|
||||
the feature on without realising it requires a hardened
|
||||
deployment topology.
|
||||
2. **Identity-only allowlist.** ``oauth2_config_mappings`` maps
|
||||
header names to ``UserAPIKeyAuth`` fields. Without an allowlist,
|
||||
an admin who maps the wrong header to ``user_role`` lets any
|
||||
caller send ``X-User-Role: proxy_admin`` and gain full admin
|
||||
privileges (Pydantic coerces the string into the enum). Only
|
||||
fields in ``ALLOWED_OAUTH2_PROXY_FIELDS`` (identity assertion
|
||||
only — see the constant's comment) may be mapped; any other
|
||||
mapping is rejected at request time so the misconfiguration
|
||||
surfaces loudly rather than as a silent privesc.
|
||||
**Identity-only allowlist.** ``oauth2_config_mappings`` maps header
|
||||
names to ``UserAPIKeyAuth`` fields. Without an allowlist, an admin
|
||||
who maps the wrong header to ``user_role`` lets any caller send
|
||||
``X-User-Role: proxy_admin`` and gain full admin privileges
|
||||
(Pydantic coerces the string into the enum). Only fields in
|
||||
``ALLOWED_OAUTH2_PROXY_FIELDS`` (identity assertion only — see the
|
||||
constant's comment) may be mapped; any other mapping is rejected at
|
||||
request time so the misconfiguration surfaces loudly rather than as
|
||||
a silent privesc.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import general_settings, premium_user
|
||||
|
||||
if premium_user is not True:
|
||||
raise ValueError(
|
||||
"Oauth2 proxy auth is an enterprise-only feature. "
|
||||
+ CommonProxyErrors.not_premium_user.value
|
||||
)
|
||||
from litellm.proxy.proxy_server import general_settings
|
||||
|
||||
verbose_proxy_logger.debug("Handling oauth2 proxy request")
|
||||
oauth2_config_mappings: Dict[str, str] = (
|
||||
|
||||
@@ -47,17 +47,15 @@ def _request_with_headers(headers: dict) -> Request:
|
||||
@pytest.fixture
|
||||
def configure_proxy(monkeypatch):
|
||||
"""
|
||||
Yields a callable that sets ``premium_user`` and
|
||||
``oauth2_config_mappings`` on the proxy_server module for the
|
||||
duration of one test. Default is premium=True with a single
|
||||
``user_id -> x-user-id`` mapping.
|
||||
Yields a callable that sets ``oauth2_config_mappings`` on the
|
||||
proxy_server module for the duration of one test. Default mapping
|
||||
is a single ``user_id -> x-user-id`` (identity-only).
|
||||
"""
|
||||
import litellm.proxy.proxy_server as proxy_server
|
||||
|
||||
def _configure(*, premium=True, mappings=None):
|
||||
def _configure(*, mappings=None):
|
||||
if mappings is None:
|
||||
mappings = {"user_id": "x-user-id"}
|
||||
monkeypatch.setattr(proxy_server, "premium_user", premium, raising=False)
|
||||
monkeypatch.setattr(
|
||||
proxy_server,
|
||||
"general_settings",
|
||||
@@ -79,15 +77,6 @@ async def test_returns_auth_for_simple_user_id_mapping(configure_proxy):
|
||||
assert auth.user_role is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_when_not_premium(configure_proxy):
|
||||
configure_proxy(premium=False)
|
||||
request = _request_with_headers({"x-user-id": "alice"})
|
||||
|
||||
with pytest.raises(ValueError, match="enterprise"):
|
||||
await handle_oauth2_proxy_request(request)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"privileged_field",
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user