mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(passthrough): default auth=True; drop enterprise gate on the safe option
Pass-through endpoints configured in
``general_settings.pass_through_endpoints`` defaulted to ``auth: false``
and the safe ``auth: true`` setting was rejected at startup unless the
operator had a LiteLLM Enterprise license. Net result: OSS deployments
had **no safe configuration** — every pass-through admins added without
remembering ``auth: true`` shipped an unauthenticated forwarder, and
remembering ``auth: true`` raised a hard "enterprise-only" error.
Three changes:
* ``litellm/proxy/_types.py`` — flip
``PassThroughGenericEndpoint.auth`` default to ``True``. Operators
who add a pass-through with no explicit ``auth`` value now get a
safe, authenticated forwarder by default. Setting ``auth: false``
remains supported for genuine public-forwarder use cases (e.g.
webhook receivers).
* ``litellm/proxy/pass_through_endpoints/pass_through_endpoints.py``
— drop the ``premium_user`` gate around ``auth: true``. An
unauthenticated forwarder is a deployment choice operators should
be allowed to make explicitly, but the safe option must always be
free. The product-tier decision (which features sit behind the
enterprise license) is separate from "OSS users must always have a
safe option."
* ``litellm/proxy/auth/user_api_key_auth.py`` — the runtime dispatch
pulls pass-through endpoints from ``general_settings`` as raw
dicts, so the Pydantic default doesn't apply. Switched
``endpoint.get("auth")`` to ``endpoint.get("auth", True)`` so a
config dict without an explicit ``auth`` key still requires
authentication at request time.
Tests:
- ``test_passthrough_auth_defaults_to_true`` — Pydantic default is
now safe.
- ``test_passthrough_auth_can_still_be_explicitly_disabled``
— opt-in to ``auth=False`` still works for legitimate
public-forwarder use cases.
- ``test_register_passthrough_with_auth_true_works_for_oss``
— ``premium_user=False`` no longer rejects ``auth=true``.
- ``test_runtime_check_treats_missing_auth_key_as_authenticated``
— raw dict without an ``auth`` key now requires auth (the
previously-unauthenticated forwarder).
- ``test_runtime_check_explicit_auth_false_still_skips_validation``
— explicit opt-in still works.
Closes GHSA-7h34-mmrh-6g58.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9b3cd5ca25
commit
148485c2a2
4 changed files with 152 additions and 9 deletions
|
|
@ -2154,8 +2154,8 @@ class PassThroughGenericEndpoint(LiteLLMPydanticObjectBase):
|
|||
description="The USD cost per request to the target endpoint. This is used to calculate the cost of the request to the target endpoint.",
|
||||
)
|
||||
auth: bool = Field(
|
||||
default=False,
|
||||
description="Whether authentication is required for the pass-through endpoint. If True, requests to the endpoint will require a valid LiteLLM API key.",
|
||||
default=True,
|
||||
description="Whether authentication is required for the pass-through endpoint. Defaults to True so a pass-through silently created without an explicit value still requires a valid LiteLLM API key — set to False only if the endpoint is meant to be a public forwarder (e.g. an unauthenticated webhook target).",
|
||||
)
|
||||
guardrails: Optional[PassThroughGuardrailsConfig] = Field(
|
||||
default=None,
|
||||
|
|
|
|||
|
|
@ -472,7 +472,12 @@ async def check_api_key_for_custom_headers_or_pass_through_endpoints(
|
|||
for endpoint in pass_through_endpoints:
|
||||
if isinstance(endpoint, dict) and endpoint.get("path", "") == route:
|
||||
## IF AUTH DISABLED
|
||||
if endpoint.get("auth") is not True:
|
||||
# Default to True: a config dict with no ``auth`` key
|
||||
# otherwise produced an unauthenticated forwarder. The
|
||||
# Pydantic ``PassThroughGenericEndpoint.auth`` default
|
||||
# is also True, but raw config dicts skip that path —
|
||||
# so this runtime check has to default to True too.
|
||||
if endpoint.get("auth", True) is not True:
|
||||
return UserAPIKeyAuth()
|
||||
## IF AUTH ENABLED
|
||||
### IF CUSTOM PARSER REQUIRED
|
||||
|
|
|
|||
|
|
@ -2325,12 +2325,14 @@ async def _register_pass_through_endpoint(
|
|||
dependencies = None
|
||||
|
||||
if auth is not None and str(auth).lower() == "true":
|
||||
if premium_user is not True:
|
||||
raise ValueError(
|
||||
"Error Setting Authentication on Pass Through Endpoint: {}".format(
|
||||
CommonProxyErrors.not_premium_user.value
|
||||
)
|
||||
)
|
||||
# Authentication on a pass-through endpoint used to be enterprise-
|
||||
# only — which left the OSS tier with no safe configuration: the
|
||||
# default was ``auth=False`` (unauthenticated forwarder) and the
|
||||
# safe ``auth=True`` raised at startup unless the operator had a
|
||||
# license. The default is now ``True`` (safe-by-default), and
|
||||
# turning it on no longer requires a license: an unauthenticated
|
||||
# forwarder is a deployment choice the operator should be allowed
|
||||
# to make explicitly, but the safe option must always be free.
|
||||
dependencies = [Depends(user_api_key_auth)]
|
||||
if path not in LiteLLMRoutes.openai_routes.value:
|
||||
LiteLLMRoutes.openai_routes.value.append(path)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
"""
|
||||
Regression tests for the pass-through endpoint auth-default fix
|
||||
(GHSA-7h34-mmrh-6g58).
|
||||
|
||||
Two failures the fix closes:
|
||||
|
||||
1. ``PassThroughGenericEndpoint.auth`` defaulted to ``False`` — an
|
||||
admin who added a pass-through to ``general_settings`` without
|
||||
explicitly setting ``auth: true`` shipped an unauthenticated
|
||||
forwarder.
|
||||
2. Setting ``auth: true`` was rejected at startup unless the operator
|
||||
had a LiteLLM Enterprise license, leaving OSS deployments with no
|
||||
safe configuration.
|
||||
|
||||
The fix flips the default to ``True`` (safe-by-default) and removes
|
||||
the enterprise gate so OSS operators can register an authenticated
|
||||
pass-through. The runtime check in ``user_api_key_auth.py`` also now
|
||||
defaults to ``True`` so a config dict (raw, not Pydantic) without an
|
||||
``auth`` key still requires authentication.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../../.."))
|
||||
|
||||
from litellm.proxy._types import PassThroughGenericEndpoint
|
||||
from litellm.proxy.auth.user_api_key_auth import (
|
||||
check_api_key_for_custom_headers_or_pass_through_endpoints,
|
||||
)
|
||||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
_register_pass_through_endpoint,
|
||||
)
|
||||
|
||||
|
||||
def test_passthrough_auth_defaults_to_true():
|
||||
# Regression: an admin who configures a pass-through without setting
|
||||
# auth explicitly used to ship an unauthenticated forwarder. The
|
||||
# default is now safe.
|
||||
endpoint = PassThroughGenericEndpoint(
|
||||
path="/canary-forwarder",
|
||||
target="https://postman-echo.com/get",
|
||||
)
|
||||
assert endpoint.auth is True
|
||||
|
||||
|
||||
def test_passthrough_auth_can_still_be_explicitly_disabled():
|
||||
# Operators who genuinely need an unauthenticated forwarder (e.g.
|
||||
# public webhook receiver) can opt in explicitly.
|
||||
endpoint = PassThroughGenericEndpoint(
|
||||
path="/public-webhook",
|
||||
target="https://example.com/webhook",
|
||||
auth=False,
|
||||
)
|
||||
assert endpoint.auth is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_passthrough_with_auth_true_works_for_oss(monkeypatch):
|
||||
# Regression: setting ``auth: true`` used to raise at startup
|
||||
# unless ``premium_user`` was True, leaving OSS with no safe
|
||||
# configuration.
|
||||
app = MagicMock(spec=FastAPI)
|
||||
visited: set = set()
|
||||
|
||||
endpoint = PassThroughGenericEndpoint(
|
||||
path="/forwarder",
|
||||
target="https://example.com",
|
||||
auth=True,
|
||||
)
|
||||
|
||||
# Should not raise; OSS premium_user=False is allowed to use auth=True.
|
||||
await _register_pass_through_endpoint(
|
||||
endpoint=endpoint,
|
||||
app=app,
|
||||
premium_user=False,
|
||||
visited_endpoints=visited,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_check_treats_missing_auth_key_as_authenticated():
|
||||
# The runtime dispatch in user_api_key_auth pulls
|
||||
# pass_through_endpoints from general_settings as raw dicts (the
|
||||
# Pydantic default never applies). A dict without an ``auth`` key
|
||||
# must default to "authenticated" — without this, the previous
|
||||
# behaviour (``endpoint.get("auth") is not True`` -> True -> empty
|
||||
# auth) ships an unauthenticated forwarder.
|
||||
request = MagicMock()
|
||||
request.headers = {}
|
||||
raw_endpoint_no_auth_key = {
|
||||
"path": "/forwarder",
|
||||
"target": "https://example.com",
|
||||
# ``auth`` deliberately omitted
|
||||
}
|
||||
|
||||
result = await check_api_key_for_custom_headers_or_pass_through_endpoints(
|
||||
request=request,
|
||||
route="/forwarder",
|
||||
pass_through_endpoints=[raw_endpoint_no_auth_key],
|
||||
api_key="sk-1234",
|
||||
)
|
||||
|
||||
# Result is the api_key string (auth is REQUIRED for this endpoint
|
||||
# — flow continues to normal key validation), NOT an empty
|
||||
# ``UserAPIKeyAuth()`` (which was the unauthenticated-forwarder
|
||||
# bug).
|
||||
assert result == "sk-1234"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_check_explicit_auth_false_still_skips_validation():
|
||||
# Operators who explicitly set ``auth: False`` get the legacy
|
||||
# behaviour — an empty UserAPIKeyAuth, no key required.
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
|
||||
request = MagicMock()
|
||||
request.headers = {}
|
||||
raw_endpoint_auth_false = {
|
||||
"path": "/public-webhook",
|
||||
"target": "https://example.com",
|
||||
"auth": False,
|
||||
}
|
||||
|
||||
result = await check_api_key_for_custom_headers_or_pass_through_endpoints(
|
||||
request=request,
|
||||
route="/public-webhook",
|
||||
pass_through_endpoints=[raw_endpoint_auth_false],
|
||||
api_key="",
|
||||
)
|
||||
|
||||
assert isinstance(result, UserAPIKeyAuth)
|
||||
Loading…
Add table
Reference in a new issue