fix(vector_stores): redact wire-protocol connection strings in management responses

A MongoDB vector store's whole credential is its connection string, and
mongodb+srv://<user>:<password>@<cluster> embeds the database password. None of
the masker's default patterns (api_key, secret, token, credential) match a key
named mongodb_connection_string, so /vector_store/list and /vector_store/info
returned it verbatim to every caller that can read a vector store.

SensitiveDataMasker gains extra_sensitive_patterns, which unions onto the
defaults instead of replacing them, and the vector-store redactor adds
"connection" so the URI is masked while mongodb_database, mongodb_collection and
the field names stay readable.
This commit is contained in:
Yuneng Jiang 2026-09-02 09:56:17 -07:00
parent 85bda43d63
commit 22d34960e5
No known key found for this signature in database
4 changed files with 84 additions and 18 deletions

View file

@ -6,33 +6,41 @@ from pydantic import BaseModel
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER
_DEFAULT_SENSITIVE_PATTERNS: Final = frozenset(
{
"password",
"secret",
"key",
"token",
"auth",
"authorization",
"credential",
# Plural form: Vertex uses ``vertex_credentials``; segment-exact
# matching otherwise misses it because "credential" != "credentials".
"credentials",
"access",
"private",
"certificate",
"fingerprint",
"tenancy",
}
)
class SensitiveDataMasker:
def __init__(
self,
sensitive_patterns: set[str] | None = None,
extra_sensitive_patterns: set[str] | None = None,
non_sensitive_overrides: set[str] | None = None,
visible_prefix: int = 4,
visible_suffix: int = 4,
mask_char: str = "*",
mask_short_values: bool = True,
):
self.sensitive_patterns = sensitive_patterns or {
"password",
"secret",
"key",
"token",
"auth",
"authorization",
"credential",
# Plural form: Vertex uses ``vertex_credentials``; segment-exact
# matching otherwise misses it because "credential" != "credentials".
"credentials",
"access",
"private",
"certificate",
"fingerprint",
"tenancy",
}
self.sensitive_patterns = (sensitive_patterns or _DEFAULT_SENSITIVE_PATTERNS) | (
extra_sensitive_patterns or frozenset()
)
# If any key segment matches one of these, the key is not considered sensitive
# even if it also matches a sensitive pattern. For example, "input_cost_per_token"
# contains "token" but "cost" overrides that — it's a pricing field, not a secret.

View file

@ -59,7 +59,10 @@ def _row_to_vector_store(row: "_VectorStoreRow") -> LiteLLM_ManagedVectorStore:
return LiteLLM_ManagedVectorStore(**row.model_dump())
_LITELLM_PARAMS_MASKER: Final = SensitiveDataMasker()
# "connection" covers wire-protocol providers whose whole credential is a URI
# (mongodb_connection_string embeds the username and password), which the
# default api_key/secret/token patterns do not match.
_LITELLM_PARAMS_MASKER: Final = SensitiveDataMasker(extra_sensitive_patterns={"connection"})
_REDACT_LITELLM_PARAMS_MAX_DEPTH: Final = 10

View file

@ -312,3 +312,22 @@ def test_mask_credentials_in_payload_masks_only_sensitive_string_leaves():
assert masked != plaintext
assert masked.startswith(plaintext[:4])
assert masked.endswith(plaintext[-4:])
def test_extra_sensitive_patterns_add_to_the_defaults():
from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker
masker = SensitiveDataMasker(extra_sensitive_patterns={"connection"})
assert masker.is_sensitive_key("mongodb_connection_string") is True
assert masker.is_sensitive_key("api_key") is True
assert masker.is_sensitive_key("aws_secret_access_key") is True
assert masker.is_sensitive_key("mongodb_database") is False
def test_extra_sensitive_patterns_do_not_leak_into_other_maskers():
from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker
SensitiveDataMasker(extra_sensitive_patterns={"connection"})
assert SensitiveDataMasker().is_sensitive_key("mongodb_connection_string") is False

View file

@ -1,3 +1,4 @@
import json
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
@ -2700,6 +2701,41 @@ class TestRedactSensitiveLitellmParams:
for k, v in params.items():
assert out[k] == v, f"{k} should be preserved verbatim"
def test_redacts_wire_protocol_connection_strings(self):
"""
A MongoDB vector store's whole credential is its connection string:
``mongodb+srv://<user>:<password>@<cluster>`` embeds the database
password, and none of the default api_key/secret/token patterns match
the key name, so an unextended masker returns it verbatim to every
caller of /vector_store/list and /vector_store/info.
"""
from litellm.constants import REDACTED_BY_LITELM_STRING
from litellm.proxy.vector_store_endpoints.management_endpoints import (
_redact_sensitive_litellm_params,
)
password = "hunter2-not-for-callers"
params = {
"mongodb_connection_string": f"mongodb+srv://dbuser:{password}@cluster0.mongodb.net",
"mongodb_database": "sample_mflix",
"mongodb_collection": "embedded_movies",
"mongodb_embedding_field": "plot_embedding",
"mongodb_text_field": "plot",
"litellm_embedding_model": "openai/text-embedding-ada-002",
}
out = _redact_sensitive_litellm_params(params)
assert out["mongodb_connection_string"] == REDACTED_BY_LITELM_STRING
assert password not in json.dumps(out)
for k in (
"mongodb_database",
"mongodb_collection",
"mongodb_embedding_field",
"mongodb_text_field",
"litellm_embedding_model",
):
assert out[k] == params[k], f"{k} is not a credential and must survive redaction"
def test_handles_none_and_empty(self):
from litellm.proxy.vector_store_endpoints.management_endpoints import (
_redact_sensitive_litellm_params,