fix(xai): reject xai_oauth_token_file in request bodies regardless of client-side opt-ins

The token file picks which OAuth account stored on the proxy host signs the
request, so allow_client_side_credentials and configurable_clientside_auth_params
must not hand it to callers. Only deployment config may set it

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
hx 2026-09-27 18:07:43 +08:00
parent f9db7d8e75
commit c9645e2dda
3 changed files with 45 additions and 53 deletions

View file

@ -437,29 +437,6 @@ def _map_openai_exception(
response=response,
litellm_debug_info=extra_information,
)
elif original_exception.status_code == 403:
from litellm.llms.xai.oauth import is_xai_spending_limit_error
if is_xai_spending_limit_error(
custom_llm_provider=custom_llm_provider,
status_code=original_exception.status_code,
error_str=error_str,
):
raise RateLimitError(
message=f"RateLimitError: {exception_provider} - {message}",
model=model,
llm_provider=custom_llm_provider,
response=getattr(original_exception, "response", None),
litellm_debug_info=extra_information,
)
raise APIError(
status_code=original_exception.status_code,
message=f"APIError: {exception_provider} - {message}",
llm_provider=custom_llm_provider,
model=model,
request=getattr(original_exception, "request", None),
litellm_debug_info=extra_information,
)
elif original_exception.status_code == 404:
raise NotFoundError(
message=f"NotFoundError: {exception_provider} - {message}",

View file

@ -303,6 +303,10 @@ 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",
@ -327,11 +331,7 @@ _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",
# xAI SuperGrok OAuth token file. The xAI transformer reads
# ``xai_oauth_token_file`` and authenticates as that local account,
# so a caller-supplied path is the same credential-selector shape as
# ``aws_profile_name`` on the Bedrock path.
"xai_oauth_token_file",
*_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
@ -391,6 +391,12 @@ def _check_banned_params(
Shared between the root-level check and the nested-config check so a
new banned param only needs to be added in one place.
"""
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

View file

@ -3945,47 +3945,56 @@ class TestIsRequestBodySafeBlocksAwsIdentitySelectors:
class TestIsRequestBodySafeBlocksXaiOauthTokenFile:
"""A caller must not select another local xAI OAuth account. Router kwargs
override deployment params, so ``xai_oauth_token_file`` in the request body
would authenticate as any token file readable on the proxy host.
"""``xai_oauth_token_file`` picks which OAuth account stored on the proxy host signs the
request, and router kwargs override deployment params, so no client-side opt-in may unlock it
"""
def test_xai_oauth_token_file_is_in_banned_set(self):
from litellm.proxy.auth.auth_utils import _BANNED_REQUEST_BODY_PARAMS
assert "xai_oauth_token_file" in set(_BANNED_REQUEST_BODY_PARAMS)
def test_xai_oauth_token_file_in_request_body_is_rejected(self):
@pytest.mark.parametrize(
"request_body",
[
{"model": "grok-4", "xai_oauth_token_file": "auth-bob.json"},
{"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"}]},
],
)
def test_rejected_even_under_proxy_wide_opt_in(self, request_body):
with pytest.raises(ValueError, match="xai_oauth_token_file"):
is_request_body_safe(
request_body={
"model": "grok-4",
"xai_oauth_token_file": "/etc/passwd",
},
general_settings={},
request_body=request_body,
general_settings={"allow_client_side_credentials": True},
llm_router=None,
model="grok-4",
)
def test_xai_oauth_token_file_under_extra_body_is_rejected(self):
def test_rejected_even_when_deployment_lists_it_as_clientside_configurable(self):
from litellm import Router
router = Router(
model_list=[
{
"model_name": "grok-4",
"litellm_params": {
"model": "xai/grok-4",
"use_xai_oauth": True,
"xai_oauth_token_file": "auth-alice.json",
"configurable_clientside_auth_params": ["xai_oauth_token_file"],
},
}
]
)
with pytest.raises(ValueError, match="xai_oauth_token_file"):
is_request_body_safe(
request_body={
"model": "grok-4",
"extra_body": {"xai_oauth_token_file": "/etc/passwd"},
},
request_body={"model": "grok-4", "xai_oauth_token_file": "auth-bob.json"},
general_settings={},
llm_router=None,
llm_router=router,
model="grok-4",
)
def test_xai_oauth_token_file_allowed_under_proxy_wide_opt_in(self):
def test_other_banned_params_still_honor_proxy_wide_opt_in(self):
assert (
is_request_body_safe(
request_body={
"model": "grok-4",
"xai_oauth_token_file": "auth-alice.json",
},
request_body={"model": "grok-4", "api_base": "https://example.invalid/v1"},
general_settings={"allow_client_side_credentials": True},
llm_router=None,
model="grok-4",