mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-01 07:03:02 +00:00
fix(xai): register xai_oauth_token_file as a server-owned param
Route the token file through the shared server-owned gate so it is refused in body litellm_params, stored key/team fallback targets and health check params, and so only proxy admins can set it on a deployment. Drops the parallel operator-only check this replaces
This commit is contained in:
parent
97eb891c17
commit
09d5ae5645
6 changed files with 24 additions and 20 deletions
|
|
@ -389,10 +389,6 @@ def _build_banned_observability_params() -> frozenset[str]:
|
|||
)
|
||||
|
||||
|
||||
# Credential selectors that name an account stored on the proxy host, so no
|
||||
# client-side opt-in can hand them to a caller. Only the deployment config sets them.
|
||||
_OPERATOR_ONLY_REQUEST_BODY_PARAMS: Final[tuple[str, ...]] = ("xai_oauth_token_file",)
|
||||
|
||||
_BANNED_REQUEST_BODY_PARAMS: Final[tuple[str, ...]] = (
|
||||
"api_base",
|
||||
"base_url",
|
||||
|
|
@ -417,7 +413,6 @@ _BANNED_REQUEST_BODY_PARAMS: Final[tuple[str, ...]] = (
|
|||
# caller-supplied value is the same exfil shape as
|
||||
# ``aws_web_identity_token`` on the Bedrock path.
|
||||
"azure_ad_token",
|
||||
*_OPERATOR_ONLY_REQUEST_BODY_PARAMS,
|
||||
# Endpoint-targeting fields that retarget the outbound request or
|
||||
# an observability callback. An attacker-controlled value either
|
||||
# exfiltrates the request payload (incl. messages + admin-set
|
||||
|
|
@ -471,6 +466,8 @@ _BANNED_REQUEST_BODY_PARAMS: Final[tuple[str, ...]] = (
|
|||
"github_copilot_host",
|
||||
"user_provider_credentials",
|
||||
"github_copilot_user_session",
|
||||
# Picks which xAI OAuth account stored on the proxy host signs the call.
|
||||
"xai_oauth_token_file",
|
||||
# Observability credentials, hosts, and project identifiers: derived
|
||||
# from the canonical ``_supported_callback_params`` allowlist so new
|
||||
# integrations are covered automatically. Sorted for stable iteration
|
||||
|
|
@ -502,12 +499,6 @@ def _check_banned_params(
|
|||
reject_server_owned_wif_params(wif_body)
|
||||
if not manages_deployments:
|
||||
reject_federated_credential_reference(body)
|
||||
operator_only: Final = next((param for param in _OPERATOR_ONLY_REQUEST_BODY_PARAMS if param in body), None)
|
||||
if operator_only is not None:
|
||||
raise ValueError(
|
||||
f"Rejected Request: {operator_only} is not allowed in request body. "
|
||||
"Set it on the deployment's litellm_params in your proxy config.yaml instead."
|
||||
)
|
||||
for param in _BANNED_REQUEST_BODY_PARAMS:
|
||||
if param not in body:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -4323,11 +4323,13 @@ def credential_api_key_secret_reference(credential_values: Mapping[str, object])
|
|||
return {**credential_values, "api_key": reference}
|
||||
|
||||
|
||||
xai_oauth_litellm_params: Final = ("xai_oauth_token_file",)
|
||||
server_owned_wif_litellm_params: Final = (
|
||||
anthropic_wif_litellm_params
|
||||
+ openai_wif_litellm_params
|
||||
+ oauth_token_exchange_litellm_params
|
||||
+ github_copilot_oauth_litellm_params
|
||||
+ xai_oauth_litellm_params
|
||||
)
|
||||
secret_bearing_wif_litellm_params: Final = tuple(sorted(WIF_SECRET_BEARING_KEYS))
|
||||
|
||||
|
|
|
|||
|
|
@ -5316,6 +5316,7 @@ class TestIsRequestBodySafeBlocksXaiOauthTokenFile:
|
|||
{"model": "grok-4", "extra_body": {"xai_oauth_token_file": "auth-bob.json"}},
|
||||
{"model": "grok-4", "metadata": {"xai_oauth_token_file": "auth-bob.json"}},
|
||||
{"model": "grok-4", "fallbacks": [{"model": "grok-4", "xai_oauth_token_file": "auth-bob.json"}]},
|
||||
{"model": "grok-4", "litellm_params": {"xai_oauth_token_file": "auth-bob.json"}},
|
||||
],
|
||||
)
|
||||
def test_rejected_even_under_proxy_wide_opt_in(self, request_body):
|
||||
|
|
@ -5327,6 +5328,12 @@ class TestIsRequestBodySafeBlocksXaiOauthTokenFile:
|
|||
model="grok-4",
|
||||
)
|
||||
|
||||
def test_rejected_in_stored_fallback_targets_and_health_check_params(self):
|
||||
from litellm.proxy.auth.auth_utils import reject_server_owned_wif_params
|
||||
|
||||
with pytest.raises(ValueError, match="xai_oauth_token_file"):
|
||||
reject_server_owned_wif_params({"model": "xai/grok-4", "xai_oauth_token_file": "auth-bob.json"})
|
||||
|
||||
def test_rejected_even_when_deployment_lists_it_as_clientside_configurable(self):
|
||||
from litellm import Router
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ from litellm.proxy.auth.auth_utils import is_request_body_safe # noqa: E402
|
|||
"base_url",
|
||||
"vertex_credentials",
|
||||
"azure_ad_token",
|
||||
"xai_oauth_token_file",
|
||||
],
|
||||
)
|
||||
def test_banned_param_under_extra_body_is_rejected(banned_param):
|
||||
|
|
|
|||
|
|
@ -8989,14 +8989,21 @@ class TestNonAdminCannotPersistWifFieldsOnModel:
|
|||
mock_prisma.db.litellm_proxymodeltable.update.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_model_non_admin_cannot_set_openai_wif_field(self):
|
||||
@pytest.mark.parametrize(
|
||||
("model", "field", "value"),
|
||||
[
|
||||
("openai/gpt-4o-mini", "openai_identity_token_file", "/var/run/secrets/tokens/attacker"),
|
||||
("xai/grok-4", "xai_oauth_token_file", "auth-other-account.json"),
|
||||
],
|
||||
)
|
||||
async def test_patch_model_non_admin_cannot_set_server_owned_token_file(self, model: str, field: str, value: str):
|
||||
from litellm.proxy.management_endpoints.model_management_endpoints import (
|
||||
patch_model,
|
||||
)
|
||||
|
||||
non_admin = UserAPIKeyAuth(user_id="team_admin", user_role=LitellmUserRoles.INTERNAL_USER)
|
||||
existing_row = MagicMock()
|
||||
existing_row.litellm_params = {"model": "openai/gpt-4o-mini"}
|
||||
existing_row.litellm_params = {"model": model}
|
||||
existing_row.model_dump.return_value = {
|
||||
"model_name": "gpt",
|
||||
"litellm_params": existing_row.litellm_params,
|
||||
|
|
@ -9033,14 +9040,10 @@ class TestNonAdminCannotPersistWifFieldsOnModel:
|
|||
) as exc_info:
|
||||
await patch_model(
|
||||
model_id="m1",
|
||||
patch_data=updateDeployment(
|
||||
litellm_params=updateLiteLLMParams(
|
||||
openai_identity_token_file="/var/run/secrets/tokens/attacker",
|
||||
)
|
||||
),
|
||||
patch_data=updateDeployment(litellm_params=updateLiteLLMParams.model_validate({field: value})),
|
||||
user_api_key_dict=non_admin,
|
||||
)
|
||||
assert getattr(exc_info.value, "param", "") == "openai_identity_token_file"
|
||||
assert getattr(exc_info.value, "param", "") == field
|
||||
mock_prisma.db.litellm_proxymodeltable.update.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from litellm.types.utils import (
|
|||
oauth_token_exchange_litellm_params,
|
||||
openai_wif_litellm_params,
|
||||
server_owned_wif_litellm_params,
|
||||
xai_oauth_litellm_params,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -367,6 +368,7 @@ def test_server_owned_registry_includes_anthropic_openai_and_oauth_token_exchang
|
|||
+ openai_wif_litellm_params
|
||||
+ oauth_token_exchange_litellm_params
|
||||
+ github_copilot_oauth_litellm_params
|
||||
+ xai_oauth_litellm_params
|
||||
)
|
||||
assert set(openai_wif_litellm_params) == {
|
||||
"openai_identity_provider_id",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue