From 0806cca34012c389defda18a398001288ac86254 Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Sat, 25 Apr 2026 04:50:39 +0000 Subject: [PATCH] chore(vector-stores): redact credentials from list/info responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``LiteLLM_ManagedVectorStore.litellm_params`` carries the upstream provider credential — OpenAI ``api_key``, AWS ``aws_access_key_id`` / ``aws_secret_access_key``, GCP ``vertex_credentials``, etc. ``GET /vector_store/list`` and ``POST /vector_store/info`` return these verbatim to any authenticated principal. Because both routes are in ``openai_routes``, ``RouteChecks.is_llm_api_route`` short-circuits the standard role gate, so even read-only users and narrowly-scoped keys can read every stored credential. Replace credential-bearing values with the ``REDACTED_BY_LITELM`` sentinel in both responses while preserving non-secret keys (``api_base``, ``region``, ``model``, ``api_version``) so callers can still see *which* upstream is configured. Detection reuses ``SensitiveDataMasker.is_sensitive_key`` with the default heuristics plus the plural ``credentials`` pattern (covers Vertex's ``vertex_credentials`` field, which the singular ``credential`` pattern misses on segment-exact matching). Applied at: - ``list_vector_stores`` (``GET /vector_store/list``, ``GET /v1/vector_store/list``) - ``get_vector_store_info`` (``POST /vector_store/info``), both the in-memory-registry path and the prisma-DB fallback Co-Authored-By: Claude Opus 4.7 (1M context) --- .../management_endpoints.py | 74 +++++++++++++++++- .../test_vector_store_endpoints.py | 76 +++++++++++++++++++ 2 files changed, 148 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/vector_store_endpoints/management_endpoints.py b/litellm/proxy/vector_store_endpoints/management_endpoints.py index fefa6cb4e94..fb064b644a5 100644 --- a/litellm/proxy/vector_store_endpoints/management_endpoints.py +++ b/litellm/proxy/vector_store_endpoints/management_endpoints.py @@ -16,7 +16,9 @@ from fastapi import APIRouter, Depends, HTTPException import litellm from litellm._logging import verbose_proxy_logger +from litellm.constants import REDACTED_BY_LITELM_STRING from litellm.litellm_core_utils.safe_json_dumps import safe_dumps +from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker from litellm.proxy._types import ( LiteLLM_ManagedVectorStoresTable, ResponseLiteLLM_ManagedVectorStore, @@ -38,6 +40,68 @@ from litellm.vector_stores.vector_store_registry import VectorStoreRegistry router = APIRouter() +# Module-level masker — extends the default sensitive-key heuristics with +# plural forms used by some providers (e.g. Vertex's ``vertex_credentials``, +# which would otherwise slip past the singular "credential" pattern). +_LITELLM_PARAMS_MASKER = SensitiveDataMasker( + sensitive_patterns={ + "password", + "secret", + "key", + "token", + "auth", + "authorization", + "credential", + "credentials", + "access", + "private", + "certificate", + "fingerprint", + "tenancy", + }, +) + + +def _redact_sensitive_litellm_params( + litellm_params: Optional[Dict[str, Any]], +) -> Optional[Dict[str, Any]]: + """ + Replace credential-bearing values inside ``litellm_params`` with the + ``REDACTED_BY_LITELM`` sentinel while preserving non-secret keys + (``api_base``, ``region``, ``model``, etc.) so callers can still see + *which* upstream is configured. + + Without this, ``/vector_store/list`` and ``/vector_store/info`` return + the raw provider credentials (OpenAI ``api_key``, AWS + ``aws_secret_access_key``, GCP ``vertex_credentials``, ...) to any + authenticated principal, including read-only users and narrowly-scoped + keys. + """ + if not litellm_params or not isinstance(litellm_params, dict): + return litellm_params + + redacted: Dict[str, Any] = {} + for k, v in litellm_params.items(): + if _LITELLM_PARAMS_MASKER.is_sensitive_key(k): + redacted[k] = REDACTED_BY_LITELM_STRING + else: + redacted[k] = v + return redacted + + +def _redact_vector_store( + vector_store: LiteLLM_ManagedVectorStore, +) -> LiteLLM_ManagedVectorStore: + """ + Return a copy of ``vector_store`` with credential-bearing fields + inside ``litellm_params`` replaced by the redaction sentinel. + """ + redacted = LiteLLM_ManagedVectorStore(**vector_store) + redacted["litellm_params"] = _redact_sensitive_litellm_params( + vector_store.get("litellm_params") + ) + return redacted + def _resolve_embedding_config_from_router( embedding_model: str, llm_router @@ -555,7 +619,7 @@ async def list_vector_stores( accessible_vector_stores = [] for vs in vector_store_map.values(): if await _check_vector_store_access(vs, user_api_key_dict): - accessible_vector_stores.append(vs) + accessible_vector_stores.append(_redact_vector_store(vs)) total_count = len(accessible_vector_stores) total_pages = (total_count + page_size - 1) // page_size @@ -716,7 +780,9 @@ async def get_vector_store_info( created_at=vector_store.get("created_at") or None, updated_at=vector_store.get("updated_at") or None, litellm_credential_name=vector_store.get("litellm_credential_name"), - litellm_params=vector_store.get("litellm_params") or None, + litellm_params=_redact_sensitive_litellm_params( + vector_store.get("litellm_params") + ), team_id=vector_store.get("team_id") or None, user_id=vector_store.get("user_id") or None, ) @@ -742,6 +808,10 @@ async def get_vector_store_info( detail="Access denied: You do not have permission to access this vector store", ) + if "litellm_params" in vector_store_dict: + vector_store_dict["litellm_params"] = _redact_sensitive_litellm_params( + vector_store_dict["litellm_params"] + ) return {"vector_store": vector_store_dict} except Exception as e: verbose_proxy_logger.exception(f"Error getting vector store info: {str(e)}") diff --git a/tests/test_litellm/proxy/vector_store_endpoints/test_vector_store_endpoints.py b/tests/test_litellm/proxy/vector_store_endpoints/test_vector_store_endpoints.py index 44cc5cc4452..57bbbab8fce 100644 --- a/tests/test_litellm/proxy/vector_store_endpoints/test_vector_store_endpoints.py +++ b/tests/test_litellm/proxy/vector_store_endpoints/test_vector_store_endpoints.py @@ -1882,3 +1882,79 @@ async def test_create_vector_store_in_db_raises_when_no_db(): assert exc_info.value.status_code == 500 assert "database not connected" in exc_info.value.detail.lower() + + +class TestRedactSensitiveLitellmParams: + """ + ``litellm_params`` on a managed vector store carries the upstream + provider credential (OpenAI ``api_key``, AWS ``aws_secret_access_key``, + GCP ``vertex_credentials``, etc.). The list/info endpoints must redact + those values before returning them to any caller — including read-only + users and narrowly-scoped keys. + """ + + def test_redacts_well_known_credential_keys(self): + from litellm.constants import REDACTED_BY_LITELM_STRING + from litellm.proxy.vector_store_endpoints.management_endpoints import ( + _redact_sensitive_litellm_params, + ) + + params = { + "api_key": "sk-real-openai-key-12345", + "aws_access_key_id": "AKIAIOSFODNN7EXAMPLE", + "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "vertex_credentials": ( + '{"type":"service_account","private_key":"-----BEGIN PRIVATE KEY-----..."}' + ), + "azure_authorization_token": "Bearer eyJhbGciOi...", + } + out = _redact_sensitive_litellm_params(params) + for k in params: + assert ( + out[k] == REDACTED_BY_LITELM_STRING + ), f"{k} should be redacted, got {out[k]!r}" + + def test_preserves_non_sensitive_keys(self): + from litellm.proxy.vector_store_endpoints.management_endpoints import ( + _redact_sensitive_litellm_params, + ) + + params = { + "api_base": "https://api.openai.com/v1", + "model": "text-embedding-3-large", + "region": "us-east-1", + "vector_store_id": "vs_abc123", + "api_version": "2023-05-15", + } + out = _redact_sensitive_litellm_params(params) + for k, v in params.items(): + assert out[k] == v, f"{k} should be preserved verbatim" + + def test_handles_none_and_empty(self): + from litellm.proxy.vector_store_endpoints.management_endpoints import ( + _redact_sensitive_litellm_params, + ) + + assert _redact_sensitive_litellm_params(None) is None + assert _redact_sensitive_litellm_params({}) == {} + + def test_redact_vector_store_does_not_mutate_input(self): + from litellm.proxy.vector_store_endpoints.management_endpoints import ( + _redact_vector_store, + ) + + original = { + "vector_store_id": "vs_abc123", + "vector_store_name": "prod-embeddings", + "litellm_params": { + "api_key": "sk-real-openai-key-12345", + "api_base": "https://api.openai.com/v1", + }, + } + snapshot = { + "vector_store_id": original["vector_store_id"], + "vector_store_name": original["vector_store_name"], + "litellm_params": dict(original["litellm_params"]), + } + _redact_vector_store(original) + assert original == snapshot, "input vector store dict must not be mutated"