fix: langfuse trace leak key on model params#22188
fix: langfuse trace leak key on model params#22188yuneng-jiang merged 3 commits intolitellm_internal_dev_03_16_2026from
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@greptile please review this PR |
Greptile SummaryThis PR fixes a security vulnerability where Langfuse traces were leaking sensitive credentials (Langfuse API keys, authorization headers, etc.) via two vectors: (1) Key changes:
Confidence Score: 3/5
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/litellm_logging.py | Expands scrub_sensitive_keys_in_metadata to remove logging and callback_settings from three metadata dicts and drop user_api_key_auth entirely. The new approach mutates nested dicts in-place (unlike the old copy-and-replace pattern), which could introduce aliasing side-effects; no new tests cover the expanded scrubbing behavior. |
| litellm/integrations/langfuse/langfuse.py | Replaces direct use of optional_params as modelParameters / model_parameters with a sanitized whitelist via ModelParamHelper.get_standard_logging_model_parameters. The v1 path (Langfuse <2) and v2 path both now use the whitelist; the old explicit secret_fields pop is removed since the whitelist approach excludes it automatically. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[LiteLLM Request] --> B[scrub_sensitive_keys_in_metadata]
B --> C{langfuse_masking_function\nin metadata?}
C -- Yes --> D[Extract to _langfuse_masking_function]
C -- No --> E
D --> E[For each: user_api_key_metadata\nuser_api_key_auth_metadata\nuser_api_key_team_metadata]
E --> F[Pop 'logging' key]
E --> G[Pop 'callback_settings' key]
E --> H[Pop 'user_api_key_auth' from metadata]
F & G & H --> I[litellm_params stored on Logging object]
I --> J{Langfuse v1 or v2?}
J -- v1 --> K[get_standard_logging_model_parameters\nfrom optional_params whitelist]
J -- v2 --> L{standard_logging_object\navailable?}
L -- Yes --> M[Use standard_logging_object.model_parameters\nalready sanitized]
L -- No --> N[get_standard_logging_model_parameters\nfrom optional_params whitelist]
K --> O[trace.generation modelParameters=sanitized]
M --> P[generation model_parameters=sanitized]
N --> P
Last reviewed commit: 9f15a99
Additional Comments (2)
On line 831, Similarly, Previously, Consider either:
This loop coerces non-primitive values to strings (required by Langfuse v1's |
| ## 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", | ||
| ): | ||
| 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 | ||
| if metadata_field in metadata and isinstance(metadata[metadata_field], dict): | ||
| for sensitive_key in _sensitive_keys: | ||
| metadata[metadata_field].pop(sensitive_key, None) | ||
|
|
||
| metadata["user_api_key_metadata"] = cleaned_user_api_key_metadata | ||
| litellm_params["metadata"] = metadata | ||
| ## remove user_api_key_auth entirely - contains full auth object with nested credentials | ||
| metadata.pop("user_api_key_auth", None) | ||
|
|
||
| litellm_params["metadata"] = metadata |
There was a problem hiding this comment.
In-place mutation of shared nested dicts
The new code mutates metadata[metadata_field] in-place using .pop(), while the old code created a fresh cleaned_user_api_key_metadata dict and replaced the reference in metadata. This is a subtle but important difference.
If the nested dict (e.g. metadata["user_api_key_metadata"]) is the same object referenced by the original kwargs["litellm_params"]["metadata"]["user_api_key_metadata"] (since scrub_sensitive_keys_in_metadata receives litellm_params without a deep copy), then mutating it in place will modify the shared object. Any other code that still holds a reference to that nested dict will observe the keys removed.
Consider replacing the .pop() with a dict comprehension (matching the previous approach of creating a new dict) to avoid unexpected aliasing side-effects:
for metadata_field in (
"user_api_key_metadata",
"user_api_key_auth_metadata",
"user_api_key_team_metadata",
):
if metadata_field in metadata and isinstance(metadata[metadata_field], dict):
metadata[metadata_field] = {
k: v
for k, v in metadata[metadata_field].items()
if k not in _sensitive_keys
}There was a problem hiding this comment.
@greptile @greptileai thats the expected desired behaviour we want
There was a problem hiding this comment.
Got it. The in-place mutation ensures sensitive keys are removed from all references to these metadata objects, not just this code path. That makes sense for preventing credential leaks.
| ## 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", | ||
| ): | ||
| 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 | ||
| if metadata_field in metadata and isinstance(metadata[metadata_field], dict): | ||
| for sensitive_key in _sensitive_keys: | ||
| metadata[metadata_field].pop(sensitive_key, None) | ||
|
|
||
| metadata["user_api_key_metadata"] = cleaned_user_api_key_metadata | ||
| litellm_params["metadata"] = metadata | ||
| ## remove user_api_key_auth entirely - contains full auth object with nested credentials | ||
| metadata.pop("user_api_key_auth", None) | ||
|
|
||
| litellm_params["metadata"] = metadata | ||
|
|
||
| return litellm_params |
There was a problem hiding this comment.
No unit tests for new scrubbing behavior
The PR does not add tests to verify the newly expanded scrubbing logic:
- That
loggingandcallback_settingsare removed fromuser_api_key_auth_metadataanduser_api_key_team_metadata(the old code only scrubbeduser_api_key_metadata) - That
user_api_key_authis entirely removed frommetadata - That the keys are completely removed (not replaced with a placeholder like the old
"scrubbed_by_litellm_for_sensitive_keys"behavior)
The existing tests in test_langfuse_unit_tests.py only cover the langfuse_masking_function extraction path, leaving the core credential-scrubbing path untested. Adding a test similar to the following would help ensure the fix is solid:
def test_scrub_sensitive_keys_removes_credentials():
from litellm.litellm_core_utils.litellm_logging import scrub_sensitive_keys_in_metadata
litellm_params = {
"metadata": {
"user_api_key_metadata": {"logging": {"langfuse_secret_key": "sk-..."}, "safe_key": "ok"},
"user_api_key_auth_metadata": {"callback_settings": {"secret": "..."}, "other": "val"},
"user_api_key_team_metadata": {"logging": {"api_key": "..."}, "name": "team-a"},
"user_api_key_auth": {"api_key": "sk-real-key", "metadata": {"logging": {...}}},
}
}
result = scrub_sensitive_keys_in_metadata(litellm_params)
meta = result["metadata"]
assert "logging" not in meta["user_api_key_metadata"]
assert meta["user_api_key_metadata"]["safe_key"] == "ok"
assert "callback_settings" not in meta["user_api_key_auth_metadata"]
assert "logging" not in meta["user_api_key_team_metadata"]
assert "user_api_key_auth" not in metaRule Used: What: Ensure that any PR claiming to fix an issue ... (source)
ad62071
into
litellm_internal_dev_03_16_2026
Leaked traces removed


Removes logging and callback_settings keys from these 3 dicts:
Removes user_api_key_auth entirely this contained the full auth object with nested metadata.logging that had the Langfuse credentials