fix(sso): gate /sso/debug routes behind ENABLE_SSO_DEBUG, off by default (#43150)

/sso/debug/login and /sso/debug/callback are diagnostic pages that had
no off switch. They cannot carry a bearer credential because the IdP
redirects a bare browser to the callback, so the gate is an explicit
opt-in flag rather than key auth: both routes return 404 unless
ENABLE_SSO_DEBUG is set to a truthy value.
This commit is contained in:
Oliver Jensen 2026-09-25 20:58:17 +02:00 • committed by GitHub
parent f6882246d4
commit 88fd15315c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 45 additions and 3 deletions

View file

@ -4618,6 +4618,13 @@ class GoogleSSOHandler:
return result or {}
def _raise_if_sso_debug_disabled() -> None:
"""The debug routes run the browser-redirect SSO flow, so they cannot carry a
bearer credential; an explicit opt-in flag is the only way to gate them."""
if get_secret_bool("ENABLE_SSO_DEBUG") is not True:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found")
@router.get("/sso/debug/login", tags=["experimental"], include_in_schema=False)
async def debug_sso_login(request: Request):
"""
@ -4625,6 +4632,8 @@ async def debug_sso_login(request: Request):
PROXY_BASE_URL should be the your deployed proxy endpoint, e.g. PROXY_BASE_URL="https://litellm-production-7002.up.railway.app/"
Example:
"""
_raise_if_sso_debug_disabled()
from litellm.proxy.proxy_server import premium_user
microsoft_client_id: Final = os.getenv("MICROSOFT_CLIENT_ID", None)
@ -4670,6 +4679,8 @@ async def debug_sso_callback(request: Request):
"""
Returns the OpenID object returned by the SSO provider
"""
_raise_if_sso_debug_disabled()
import json
from fastapi.responses import HTMLResponse

View file

@ -8029,6 +8029,37 @@ class TestPKCEStateCookieBinding:
assert result is not None
@pytest.mark.asyncio
@pytest.mark.parametrize("enable_sso_debug_value", [None, "false", "0"])
async def test_sso_debug_routes_return_404_unless_explicitly_enabled(enable_sso_debug_value):
"""
/sso/debug/login and /sso/debug/callback must 404 unless ENABLE_SSO_DEBUG is
explicitly set to a truthy value.
"""
from litellm.proxy.management_endpoints.ui_sso import debug_sso_callback, debug_sso_login
mock_request = MagicMock(spec=Request)
mock_request.base_url = "http://proxy.example.com/"
mock_request.cookies = {}
mock_request.query_params = {}
env = {"GENERIC_CLIENT_ID": "test_client_id"}
if enable_sso_debug_value is not None:
env["ENABLE_SSO_DEBUG"] = enable_sso_debug_value
with patch.dict(os.environ, env, clear=False):
if enable_sso_debug_value is None:
os.environ.pop("ENABLE_SSO_DEBUG", None)
with pytest.raises(HTTPException) as login_exc:
await debug_sso_login(mock_request)
with pytest.raises(HTTPException) as callback_exc:
await debug_sso_callback(mock_request)
assert login_exc.value.status_code == 404
assert callback_exc.value.status_code == 404
@pytest.mark.asyncio
async def test_debug_sso_callback_renders_full_jwt_claims():
"""
@ -8080,7 +8111,7 @@ async def test_debug_sso_callback_renders_full_jwt_claims():
with (
patch.dict(
os.environ,
{"GENERIC_CLIENT_ID": "test_client_id"},
{"GENERIC_CLIENT_ID": "test_client_id", "ENABLE_SSO_DEBUG": "true"},
clear=False,
),
patch(
@ -8165,7 +8196,7 @@ async def test_debug_sso_callback_handles_missing_raw_response():
with (
patch.dict(
os.environ,
{"MICROSOFT_CLIENT_ID": "test_microsoft_id"},
{"MICROSOFT_CLIENT_ID": "test_microsoft_id", "ENABLE_SSO_DEBUG": "true"},
clear=False,
),
patch.object(
@ -8213,7 +8244,7 @@ async def _render_debug_page(provider_env, id_jag_registered, force_inert=False)
return parsed
stack = [
patch.dict(os.environ, provider_env, clear=False),
patch.dict(os.environ, {**provider_env, "ENABLE_SSO_DEBUG": "true"}, clear=False),
patch( # test-quality-ok: endpoint test stubs the upstream generic IdP boundary
"litellm.proxy.management_endpoints.ui_sso.get_generic_sso_response", side_effect=fake_generic
),