From fbcfd59b1a23edc17d6a93880726f26e308efedd Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Wed, 29 Apr 2026 23:04:05 +0000 Subject: [PATCH] fix(oauth2-proxy): drop premium gate; identity-only allowlist is the security fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- litellm/proxy/auth/oauth2_proxy_hook.py | 36 +++++++------------ .../proxy/auth/test_oauth2_proxy_hook.py | 19 +++------- 2 files changed, 16 insertions(+), 39 deletions(-) diff --git a/litellm/proxy/auth/oauth2_proxy_hook.py b/litellm/proxy/auth/oauth2_proxy_hook.py index 1ba1b100a8..389a5b2b9e 100644 --- a/litellm/proxy/auth/oauth2_proxy_hook.py +++ b/litellm/proxy/auth/oauth2_proxy_hook.py @@ -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] = ( diff --git a/tests/test_litellm/proxy/auth/test_oauth2_proxy_hook.py b/tests/test_litellm/proxy/auth/test_oauth2_proxy_hook.py index e73ac571d5..42af9e6f03 100644 --- a/tests/test_litellm/proxy/auth/test_oauth2_proxy_hook.py +++ b/tests/test_litellm/proxy/auth/test_oauth2_proxy_hook.py @@ -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", [