Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 4 additions & 21 deletions litellm/integrations/langfuse/langfuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
reconstruct_model_name,
filter_exceptions_from_params,
)
from litellm.litellm_core_utils.model_param_helper import ModelParamHelper
from litellm.litellm_core_utils.redact_messages import redact_user_api_key_info
from litellm.integrations.langfuse.langfuse_mock_client import (
create_mock_langfuse_client,
Expand Down Expand Up @@ -292,6 +291,8 @@ def log_event_on_langfuse(

functions = optional_params.pop("functions", None)
tools = optional_params.pop("tools", None)
# Remove secret_fields to prevent leaking sensitive data (e.g., authorization headers)
optional_params.pop("secret_fields", None)
if functions is not None:
prompt["functions"] = functions
if tools is not None:
Expand Down Expand Up @@ -504,18 +505,13 @@ def _log_langfuse_v1(
kwargs.get("model", ""), custom_llm_provider, metadata
)

# Use whitelisted model parameters to prevent leaking secrets
sanitized_model_params = ModelParamHelper.get_standard_logging_model_parameters(
optional_params
)

trace.generation(
CreateGeneration(
name=metadata.get("generation_name", "litellm-completion"),
startTime=start_time,
endTime=end_time,
model=model_name,
modelParameters=sanitized_model_params,
modelParameters=optional_params,
prompt=input,
Comment on lines 513 to 515
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Credential leak in Langfuse v1 model parameters

After this revert, the raw optional_params dict (with only secret_fields popped) is passed directly to Langfuse as modelParameters. Fields like api_key, api_base, authorization, and headers can still leak to Langfuse traces.

The existing test test_langfuse_model_parameters_no_secret_leakage explicitly verifies that these fields must NOT appear in the logged parameters:

# Sensitive params that must NOT leak
"api_key": "sk-secret-key-12345",
"api_base": "https://my-private-endpoint.com",
"authorization": "Bearer sk-another-secret",
"headers": {"X-Api-Key": "secret-header-value"},

Popping only secret_fields is insufficient — these other keys will still pass through to Langfuse. The whitelist approach from ModelParamHelper.get_standard_logging_model_parameters() was what prevented leakage of all non-standard keys.

completion=output,
usage={
Expand Down Expand Up @@ -835,26 +831,13 @@ def _log_langfuse_v2( # noqa: PLR0915
kwargs.get("model", ""), custom_llm_provider, metadata
)

# Use whitelisted model_parameters from StandardLoggingPayload
# to prevent leaking secrets (api_key, auth headers, etc.)
if standard_logging_object is not None:
sanitized_model_params = standard_logging_object.get(
"model_parameters", optional_params
)
else:
sanitized_model_params = (
ModelParamHelper.get_standard_logging_model_parameters(
optional_params
)
)

generation_params = {
"name": generation_name,
"id": clean_metadata.pop("generation_id", generation_id),
"start_time": start_time,
"end_time": end_time,
"model": model_name,
"model_parameters": sanitized_model_params,
"model_parameters": optional_params,
"input": input if not mask_input else "redacted-by-litellm",
Comment on lines 839 to 841
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Credential leak in Langfuse v2 model parameters

Same credential leak issue as the v1 path: optional_params is passed directly to Langfuse's generation() without whitelist filtering. Fields such as api_key, authorization, and headers present in optional_params will be recorded in Langfuse traces.

The removed logic previously fell back to ModelParamHelper.get_standard_logging_model_parameters() when no standard_logging_object was available, providing a last-resort sanitisation layer. That safety net no longer exists.

"output": output if not mask_output else "redacted-by-litellm",
"usage": usage,
Expand Down
28 changes: 13 additions & 15 deletions litellm/litellm_core_utils/litellm_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5569,23 +5569,21 @@ def scrub_sensitive_keys_in_metadata(litellm_params: Optional[dict]):
litellm_params["_langfuse_masking_function"] = masking_fn
litellm_params["metadata"] = metadata

## remove sensitive logging/callback keys from metadata dicts
## these contain credentials (langfuse_secret_key, langfuse_public_key, etc.)
_sensitive_keys = {"logging", "callback_settings"}

for metadata_field in (
"user_api_key_metadata",
"user_api_key_auth_metadata",
"user_api_key_team_metadata",
## check user_api_key_metadata for sensitive logging keys
cleaned_user_api_key_metadata = {}
if "user_api_key_metadata" in metadata and isinstance(
metadata["user_api_key_metadata"], dict
):
if metadata_field in metadata and isinstance(metadata[metadata_field], dict):
for sensitive_key in _sensitive_keys:
metadata[metadata_field].pop(sensitive_key, None)

## remove user_api_key_auth entirely - contains full auth object with nested credentials
metadata.pop("user_api_key_auth", None)
for k, v in metadata["user_api_key_metadata"].items():
if k == "logging": # prevent logging user logging keys
cleaned_user_api_key_metadata[k] = (
"scrubbed_by_litellm_for_sensitive_keys"
)
else:
cleaned_user_api_key_metadata[k] = v

litellm_params["metadata"] = metadata
metadata["user_api_key_metadata"] = cleaned_user_api_key_metadata
litellm_params["metadata"] = metadata
Comment on lines +5572 to +5586
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Multiple sensitive metadata fields no longer scrubbed

The original scrub_sensitive_keys_in_metadata removed three categories of sensitive data that are no longer handled after this revert:

  1. user_api_key_auth entire key — the old code explicitly called metadata.pop("user_api_key_auth", None) with the comment "contains full auth object with nested credentials". That object (a UserAPIKeyAuth instance) may include team secrets, org tokens, and other nested credentials. It is no longer removed.

  2. callback_settings — removed from all three metadata fields (user_api_key_metadata, user_api_key_auth_metadata, user_api_key_team_metadata) previously. These can contain callback API keys (e.g., Langfuse secret keys). Only user_api_key_metadata is touched now, and only the logging key, not callback_settings.

  3. user_api_key_auth_metadata and user_api_key_team_metadata — the logging key inside these two fields is no longer scrubbed. This means per-team / per-key logging credentials could reach Langfuse.

Additionally, the litellm_params["metadata"] = metadata write-back is now inside the if "user_api_key_metadata" in metadata block. If the incoming metadata dict was resolved via the or {} fallback (i.e., it's a freshly allocated dict), any mutations will not be persisted back to litellm_params when user_api_key_metadata is absent.


return litellm_params

Expand Down
Loading