Merge pull request #26827 from stuxf/fix/passthrough-auth-default

chore(passthrough): default auth=True and drop enterprise gate on the safe option
This commit is contained in:
yuneng-jiang 2026-04-30 17:06:37 -07:00 committed by GitHub
commit 71d5015975
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 152 additions and 10 deletions

View File

@ -2161,8 +2161,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,

View File

@ -473,7 +473,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

View File

@ -41,7 +41,6 @@ from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
from litellm.passthrough import BasePassthroughUtils
from litellm.proxy._types import (
CommonProxyErrors,
ConfigFieldInfo,
ConfigFieldUpdate,
LiteLLMRoutes,
@ -2325,12 +2324,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)

View File

@ -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)