mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
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:
parent
9b3cd5ca25
commit
0806cca340
2 changed files with 148 additions and 2 deletions
|
|
@ -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)}")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue