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:
user
2026-04-29 23:04:05 +00:00
co-authored by Claude Opus 4.7
parent b35287a062
commit fbcfd59b1a
2 changed files with 16 additions and 39 deletions
+12 -24
View File
@@ -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",
[