chore(vector-stores): redact credentials from list/info responses

``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) <noreply@anthropic.com>
This commit is contained in:
user 2026-04-25 04:50:39 +00:00
parent 9b3cd5ca25
commit 0806cca340
No known key found for this signature in database
2 changed files with 148 additions and 2 deletions

View file

@ -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)}")

View file

@ -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"