mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-17 23:51:30 +00:00
fix(langfuse): strip callback credentials from user_api_key_auth_metadata at source
Root cause: get_sanitized_user_information_from_key() passed user_api_key_dict.metadata (which contains the raw 'logging' key with langfuse_secret_key, api keys, etc.) directly into user_api_key_auth_metadata. This dict then propagated to every logger integration via StandardLoggingUserAPIKeyMetadata without ever being scrubbed. Structural fixes (three layers of defense): 1. _safe_key_metadata() in litellm_pre_call_utils.py: strips 'logging' and 'callback_settings' from key metadata before it becomes user_api_key_auth_metadata. Credentials are now removed at the earliest possible point — before scatter. 2. langfuse.py _log_langfuse_v2: add 'user_api_key_auth_metadata' to the clean_metadata exclusion list, alongside 'user_api_key_auth'. Belt-and-suspenders for Langfuse. 3. langfuse.py: use standard_logging_object['model_parameters'] (OpenAI-spec whitelist) instead of raw optional_params for modelParameters. Structurally prevents any callback credential from reaching modelParameters regardless of future key types. These fixes are general: they protect against leaking any credential type (LLM api keys, virtual keys, etc.), not just langfuse_secret_key.
This commit is contained in:
parent
35a5381b83
commit
8efb70e7a0
3 changed files with 118 additions and 4 deletions
|
|
@ -285,7 +285,18 @@ class LangFuseLogger:
|
|||
litellm_params.get("metadata", {}) or {}
|
||||
) # if litellm_params['metadata'] == None
|
||||
metadata = self.add_metadata_from_header(litellm_params, metadata)
|
||||
optional_params = safe_deep_copy(kwargs.get("optional_params", {}))
|
||||
# Prefer model_parameters from standard_logging_object — it is pre-filtered
|
||||
# through ModelParamHelper.get_standard_logging_model_parameters() which
|
||||
# whitelists only real OpenAI API params. This structurally prevents callback
|
||||
# credentials (langfuse_secret_key, api keys, etc.) from ever reaching
|
||||
# modelParameters, regardless of what per-key callback_vars are set.
|
||||
_standard_logging_object = kwargs.get("standard_logging_object")
|
||||
if _standard_logging_object is not None:
|
||||
optional_params = dict(
|
||||
_standard_logging_object.get("model_parameters") or {}
|
||||
)
|
||||
else:
|
||||
optional_params = safe_deep_copy(kwargs.get("optional_params", {}))
|
||||
|
||||
prompt = {"messages": kwargs.get("messages")}
|
||||
|
||||
|
|
@ -293,8 +304,8 @@ class LangFuseLogger:
|
|||
tools = optional_params.pop("tools", None)
|
||||
# Remove secret_fields to prevent leaking sensitive data (e.g., authorization headers)
|
||||
optional_params.pop("secret_fields", None)
|
||||
# Remove Langfuse credential keys - these are per-key/team callback vars that should
|
||||
# not appear in modelParameters (they would expose secret keys in traces)
|
||||
# Belt-and-suspenders: also pop any credential keys that may be present
|
||||
# in the fallback path (direct litellm usage without standard_logging_object)
|
||||
optional_params.pop("langfuse_secret_key", None)
|
||||
optional_params.pop("langfuse_secret", None)
|
||||
optional_params.pop("langfuse_public_key", None)
|
||||
|
|
@ -598,6 +609,15 @@ class LangFuseLogger:
|
|||
"endpoint",
|
||||
"caching_groups",
|
||||
"previous_models",
|
||||
# user_api_key_auth is the full UserAPIKeyAuth object — it contains
|
||||
# metadata.logging[].callback_vars with per-key secrets such as
|
||||
# langfuse_secret_key, api keys, etc. Never log this object.
|
||||
"user_api_key_auth",
|
||||
# user_api_key_auth_metadata carries the raw virtual-key metadata
|
||||
# dict including the `logging` key with callback_vars/credentials.
|
||||
# The per-key identity fields (alias, team, spend, etc.) are already
|
||||
# captured in the dedicated user_api_key_* generation fields.
|
||||
"user_api_key_auth_metadata",
|
||||
]:
|
||||
continue
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -667,10 +667,29 @@ class LiteLLMProxyRequestSetup:
|
|||
if user_api_key_dict.budget_reset_at
|
||||
else None
|
||||
),
|
||||
user_api_key_auth_metadata=user_api_key_dict.metadata,
|
||||
user_api_key_auth_metadata=LiteLLMProxyRequestSetup._safe_key_metadata(
|
||||
user_api_key_dict.metadata
|
||||
),
|
||||
)
|
||||
return user_api_key_logged_metadata
|
||||
|
||||
@staticmethod
|
||||
def _safe_key_metadata(metadata: Optional[dict]) -> Optional[dict]:
|
||||
"""
|
||||
Returns a copy of the virtual-key metadata with sensitive callback config stripped.
|
||||
|
||||
The `logging` key contains per-key callback_vars (langfuse_secret_key, api keys,
|
||||
etc.) that must never be propagated into observability payloads. Similarly,
|
||||
`callback_settings` holds team-level callback secrets. Strip both before the dict
|
||||
is stored in StandardLoggingUserAPIKeyMetadata and propagated to every logger.
|
||||
"""
|
||||
if not metadata:
|
||||
return metadata
|
||||
safe = dict(metadata)
|
||||
safe.pop("logging", None)
|
||||
safe.pop("callback_settings", None)
|
||||
return safe
|
||||
|
||||
@staticmethod
|
||||
def add_user_api_key_auth_to_request_metadata(
|
||||
data: dict,
|
||||
|
|
|
|||
|
|
@ -1817,3 +1817,78 @@ async def test_bearer_token_not_in_debug_logs():
|
|||
f"Bearer token leaked in debug logs. "
|
||||
f"Found token in log output:\n{log_output[:500]}"
|
||||
)
|
||||
|
||||
|
||||
def test_safe_key_metadata_strips_logging_key():
|
||||
"""
|
||||
_safe_key_metadata must strip the `logging` key (contains raw callback_vars /
|
||||
credentials) before the dict is stored in StandardLoggingUserAPIKeyMetadata
|
||||
and propagated to every logger integration.
|
||||
"""
|
||||
raw_metadata = {
|
||||
"team": "engineering",
|
||||
"logging": [
|
||||
{
|
||||
"callback_name": "langfuse",
|
||||
"langfuse_secret_key": "sk-lf-super-secret",
|
||||
"langfuse_public_key": "pk-lf-public",
|
||||
}
|
||||
],
|
||||
}
|
||||
result = LiteLLMProxyRequestSetup._safe_key_metadata(raw_metadata)
|
||||
assert result is not None
|
||||
assert "logging" not in result, "logging key with raw credentials must be stripped"
|
||||
assert result.get("team") == "engineering", "non-sensitive keys must be preserved"
|
||||
|
||||
|
||||
def test_safe_key_metadata_strips_callback_settings_key():
|
||||
"""
|
||||
_safe_key_metadata must strip `callback_settings` (team-level callback secrets).
|
||||
"""
|
||||
raw_metadata = {
|
||||
"project": "demo",
|
||||
"callback_settings": {
|
||||
"langfuse": {"langfuse_secret_key": "sk-lf-team-secret"}
|
||||
},
|
||||
}
|
||||
result = LiteLLMProxyRequestSetup._safe_key_metadata(raw_metadata)
|
||||
assert result is not None
|
||||
assert "callback_settings" not in result, "callback_settings must be stripped"
|
||||
assert result.get("project") == "demo"
|
||||
|
||||
|
||||
def test_safe_key_metadata_none_and_empty():
|
||||
"""
|
||||
_safe_key_metadata must handle None and empty dicts without error.
|
||||
"""
|
||||
assert LiteLLMProxyRequestSetup._safe_key_metadata(None) is None
|
||||
assert LiteLLMProxyRequestSetup._safe_key_metadata({}) == {}
|
||||
|
||||
|
||||
def test_get_sanitized_user_information_from_key_excludes_credentials():
|
||||
"""
|
||||
get_sanitized_user_information_from_key must not expose per-key callback
|
||||
credentials (e.g. langfuse_secret_key) in user_api_key_auth_metadata.
|
||||
"""
|
||||
user_api_key_dict = UserAPIKeyAuth(
|
||||
api_key="hashed-test-key",
|
||||
key_alias="my-key",
|
||||
metadata={
|
||||
"team": "engineering",
|
||||
"logging": [
|
||||
{
|
||||
"callback_name": "langfuse",
|
||||
"langfuse_secret_key": "sk-lf-should-not-appear",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
result = LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key(
|
||||
user_api_key_dict
|
||||
)
|
||||
auth_metadata = result.get("user_api_key_auth_metadata") or {}
|
||||
assert "logging" not in auth_metadata, (
|
||||
"logging key (contains raw credentials) must not appear in "
|
||||
"user_api_key_auth_metadata exposed to loggers"
|
||||
)
|
||||
assert auth_metadata.get("team") == "engineering"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue