fix(vector_stores): block caller-supplied embedding selection params on query surfaces

This commit is contained in:
mateo-berri 2026-09-01 13:29:25 -07:00
parent 8b5ae3da9d
commit 8b0441a628
4 changed files with 82 additions and 0 deletions

View file

@ -42,6 +42,7 @@ from litellm.proxy.rag_endpoints.upload_security import (
)
from litellm.proxy.vector_store_endpoints.endpoints import (
build_request_data_from_managed_vector_store,
reject_caller_embedding_selection_params,
)
from litellm.proxy.vector_store_endpoints.utils import (
assert_user_can_access_vector_store_id,
@ -716,6 +717,7 @@ async def rag_query(
status_code=400,
detail={"error": "retrieval_config must contain 'vector_store_id'"},
)
reject_caller_embedding_selection_params(payload=retrieval_config, source="retrieval_config")
resolved_stores: Final = await _authorize_nested_vector_store_ids(
payload=retrieval_config,
user_api_key_dict=user_api_key_dict,

View file

@ -29,6 +29,29 @@ from litellm.types.vector_stores import IndexCreateRequest, IndexListResponse
from litellm.vector_stores.vector_store_registry import VectorStoreIndexRegistry
router: Final = APIRouter()
BLOCKED_QUERY_EMBEDDING_SELECTION_PARAMS: Final = frozenset(
{
"embedding_model",
"litellm_embedding_model",
"litellm_embedding_config",
"litellm_credential_name",
}
)
def reject_caller_embedding_selection_params(payload: Mapping[str, object], source: str) -> None:
blocked: Final = sorted(BLOCKED_QUERY_EMBEDDING_SELECTION_PARAMS & payload.keys())
if blocked:
raise HTTPException(
status_code=400,
detail={
"error": f"'{blocked[0]}' cannot be set in {source}. "
"Embedding configuration comes from the vector store's server-side registration."
},
)
########################################################
# OpenAI Compatible Endpoints
########################################################
@ -134,6 +157,7 @@ async def vector_store_search(
)
data = await _read_request_body(request=request)
reject_caller_embedding_selection_params(payload=data, source="the search request body")
data["vector_store_id"] = vector_store_id
# Check for legacy vector store registry (non-managed vector stores)

View file

@ -421,6 +421,30 @@ def test_rag_query_store_params_win_over_user_retrieval_config(client_internal_u
assert forwarded_config["aws_region_name"] == "eu-west-1"
@pytest.mark.parametrize(
"blocked_key",
["embedding_model", "litellm_embedding_model", "litellm_embedding_config", "litellm_credential_name"],
)
def test_rag_query_rejects_caller_embedding_selection_params(client_internal_user, blocked_key):
"""
Regression: a caller must not pick the embedding model or credential used at
search time. Those resolve through the Router with the proxy's credentials,
bypassing the key's model permissions, so they may only come from the
managed store's server-side registration.
"""
response = client_internal_user.post(
"/v1/rag/query",
json={
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "hello"}],
"retrieval_config": {"vector_store_id": "s3-store", blocked_key: "attacker-choice"},
},
)
assert response.status_code == 400, response.json()
assert blocked_key in str(response.json())
EICAR = r"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
INGEST_REQUEST = '{"ingest_options":{"vector_store":{"custom_llm_provider":"openai"}}}'

View file

@ -3158,3 +3158,35 @@ class TestAzureAIAnalyzeNamedIndexClassification:
user_api_key_dict=self._team_member("analyze", ["read"]),
)
assert result is True
@pytest.mark.parametrize(
"blocked_key",
["embedding_model", "litellm_embedding_model", "litellm_embedding_config", "litellm_credential_name"],
)
def test_vector_store_search_rejects_caller_embedding_selection_params(blocked_key):
"""
Regression: the search request body must not pick the embedding model or
credential used to embed the query. Those resolve through the Router with
the proxy's credentials, bypassing the key's model permissions, so they may
only come from the managed store's server-side registration.
"""
from fastapi.testclient import TestClient
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.proxy_server import app
mock_auth = UserAPIKeyAuth(user_id="test_internal_user", user_role=LitellmUserRoles.INTERNAL_USER.value)
original_overrides = app.dependency_overrides.copy()
app.dependency_overrides[user_api_key_auth] = lambda: mock_auth
try:
client = TestClient(app)
response = client.post(
"/v1/vector_stores/s3-store/search",
json={"query": "hello", blocked_key: "attacker-choice"},
)
finally:
app.dependency_overrides = original_overrides
assert response.status_code == 400, response.json()
assert blocked_key in str(response.json())