mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
Merge pull request #27903 from BerriAI/backport/27892-litellm_1.84.0rc2
fix: backport #27892 to litellm_1.84.0rc2
This commit is contained in:
commit
087003ea04
4 changed files with 135 additions and 0 deletions
|
|
@ -13,6 +13,7 @@ from litellm.constants import STANDARD_CUSTOMER_ID_HEADERS
|
|||
from litellm.litellm_core_utils.url_utils import SSRFError, validate_url
|
||||
from litellm.proxy._types import *
|
||||
from litellm.types.router import CONFIGURABLE_CLIENTSIDE_AUTH_PARAMS
|
||||
from litellm.types.utils import CustomPricingLiteLLMParams
|
||||
|
||||
|
||||
def _get_request_ip_address(
|
||||
|
|
@ -276,6 +277,7 @@ _BANNED_REQUEST_BODY_PARAMS: Tuple[str, ...] = (
|
|||
# integrations are covered automatically. Sorted for stable iteration
|
||||
# order and reviewable diffs.
|
||||
*sorted(_build_banned_observability_params()),
|
||||
*sorted(CustomPricingLiteLLMParams.model_fields.keys()),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from litellm.proxy.common_utils.http_parsing_utils import (
|
|||
_safe_get_request_headers,
|
||||
get_form_data,
|
||||
)
|
||||
from litellm.proxy.auth.auth_utils import is_request_body_safe
|
||||
from litellm.proxy.vector_store_endpoints.utils import (
|
||||
assert_user_can_access_vector_store_id,
|
||||
)
|
||||
|
|
@ -469,6 +470,16 @@ async def rag_ingest(
|
|||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
try:
|
||||
is_request_body_safe(
|
||||
request_body=ingest_options.get("vector_store", {}),
|
||||
general_settings=general_settings,
|
||||
llm_router=llm_router,
|
||||
model="",
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail={"error": str(e)})
|
||||
|
||||
# Add litellm data
|
||||
request_data: Dict[str, Any] = {}
|
||||
request_data = await add_litellm_data_to_request(
|
||||
|
|
|
|||
|
|
@ -1514,3 +1514,62 @@ def test_observability_ban_covers_canonical_supported_callback_params():
|
|||
f"{param} is in _request_blocked_callback_params but is not banned "
|
||||
"at the proxy request-body boundary."
|
||||
)
|
||||
|
||||
|
||||
# ── pricing injection (global model cost registry poisoning) ──────────────────
|
||||
|
||||
|
||||
class TestPricingInjectionBlocked:
|
||||
"""Authenticated clients must not be able to mutate the global
|
||||
litellm.model_cost registry by supplying pricing fields in the request
|
||||
body. Any CustomPricingLiteLLMParams field (input_cost_per_token etc.)
|
||||
passed to completion() is forwarded to register_model(), which overwrites
|
||||
the shared global dict for ALL users on the instance.
|
||||
|
||||
Fix: all CustomPricingLiteLLMParams fields are in _BANNED_REQUEST_BODY_PARAMS,
|
||||
so is_request_body_safe() rejects them before they reach completion().
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field,value",
|
||||
[
|
||||
("input_cost_per_token", -0.01),
|
||||
("output_cost_per_token", 0.0),
|
||||
("input_cost_per_second", 999.0),
|
||||
("output_cost_per_second", -1.0),
|
||||
("cache_read_input_token_cost", 0.0),
|
||||
("cache_creation_input_token_cost", -0.05),
|
||||
],
|
||||
)
|
||||
def test_pricing_field_rejected_by_default(self, field, value):
|
||||
with pytest.raises(ValueError) as exc:
|
||||
is_request_body_safe(
|
||||
request_body={"model": "gpt-4", field: value},
|
||||
general_settings={},
|
||||
llm_router=None,
|
||||
model="gpt-4",
|
||||
)
|
||||
assert field in str(exc.value)
|
||||
|
||||
def test_all_custom_pricing_fields_are_banned(self):
|
||||
from litellm.proxy.auth.auth_utils import _BANNED_REQUEST_BODY_PARAMS
|
||||
from litellm.types.utils import CustomPricingLiteLLMParams
|
||||
|
||||
banned = set(_BANNED_REQUEST_BODY_PARAMS)
|
||||
for field in CustomPricingLiteLLMParams.model_fields:
|
||||
assert field in banned, (
|
||||
f"CustomPricingLiteLLMParams.{field} is not in "
|
||||
"_BANNED_REQUEST_BODY_PARAMS — clients can poison the global "
|
||||
"model cost registry by supplying it in the request body."
|
||||
)
|
||||
|
||||
def test_pricing_field_allowed_with_admin_opt_in(self):
|
||||
assert (
|
||||
is_request_body_safe(
|
||||
request_body={"model": "gpt-4", "input_cost_per_token": 0.00001},
|
||||
general_settings={"allow_client_side_credentials": True},
|
||||
llm_router=None,
|
||||
model="gpt-4",
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
|
|
|||
|
|
@ -128,3 +128,66 @@ def test_internal_user_rag_ingest_without_vector_store_id_allowed(client_interna
|
|||
f"internal_user should be allowed to create new vector stores. "
|
||||
f"Response: {response.json()}"
|
||||
)
|
||||
|
||||
|
||||
class TestRagIngestSSRFBlocked:
|
||||
"""
|
||||
aws_sts_endpoint and related credential-redirect fields must be rejected
|
||||
in ingest_options.vector_store. Without this guard, any authenticated
|
||||
client can coerce the proxy to make a signed STS AssumeRole call to an
|
||||
attacker-controlled server, leaking the instance profile credentials.
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field,value",
|
||||
[
|
||||
("aws_sts_endpoint", "https://attacker.example/sts"),
|
||||
("aws_web_identity_token", "fake-token"),
|
||||
("aws_bedrock_runtime_endpoint", "https://attacker.example/bedrock"),
|
||||
],
|
||||
)
|
||||
def test_ssrf_field_in_vector_store_config_rejected(
|
||||
self, field, value, client_internal_user
|
||||
):
|
||||
payload = {
|
||||
"file_url": "https://example.com/doc.pdf",
|
||||
"ingest_options": {
|
||||
"vector_store": {
|
||||
"custom_llm_provider": "bedrock",
|
||||
field: value,
|
||||
}
|
||||
},
|
||||
}
|
||||
response = client_internal_user.post(
|
||||
"/v1/rag/ingest",
|
||||
json=payload,
|
||||
)
|
||||
assert response.status_code == 400, (
|
||||
f"{field} in ingest_options.vector_store should be rejected (400), "
|
||||
f"got {response.status_code}: {response.json()}"
|
||||
)
|
||||
body = response.json()
|
||||
detail = body.get("detail", {})
|
||||
error_text = (
|
||||
detail.get("error", "") if isinstance(detail, dict) else str(detail)
|
||||
)
|
||||
assert field in error_text, f"Error should name the offending field: {error_text}"
|
||||
|
||||
def test_clean_bedrock_ingest_options_not_rejected(self, client_internal_user):
|
||||
with patch(
|
||||
"litellm.proxy.rag_endpoints.endpoints.litellm.aingest",
|
||||
new_callable=AsyncMock,
|
||||
return_value={"vector_store_id": "vs_bedrock", "file_id": "file_123"},
|
||||
):
|
||||
response = client_internal_user.post(
|
||||
"/v1/rag/ingest",
|
||||
json={
|
||||
"file_url": "https://example.com/doc.pdf",
|
||||
"ingest_options": {
|
||||
"vector_store": {"custom_llm_provider": "bedrock"}
|
||||
},
|
||||
},
|
||||
)
|
||||
assert response.status_code != 400, (
|
||||
f"Clean Bedrock ingest_options should not be rejected: {response.json()}"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue