From a5164fe210245ab540511111bce92bdaeb2a9f5b Mon Sep 17 00:00:00 2001 From: Aanchal Khandelwal Date: Thu, 30 Jul 2026 17:37:05 -0400 Subject: [PATCH] fix(datadog): read team callback dd_* params from kwargs instead of blocked dynamic params (#35115) Team-scoped DD credentials (dd_api_key, dd_site) set via POST /team/{id}/callback were silently dropped because _request_blocked_callback_params blocks them from standard_callback_dynamic_params. The security block is correct for request-level injection, but team callback_vars are admin-configured and trusted. Store the raw init kwargs on the Logging instance and read dd_* params from there in _process_dynamic_callback_list instead of from standard_callback_dynamic_params. Adds an integration test that exercises the full Logging.__init__ flow with team callback_vars to prevent regression. --- litellm/litellm_core_utils/litellm_logging.py | 9 ++-- litellm/proxy/litellm_pre_call_utils.py | 12 +++++ .../datadog/test_datadog_team_handler.py | 52 +++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 973048814f1..c87c0690807 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -380,6 +380,7 @@ class Logging(LiteLLMLoggingBaseClass): self.standard_callback_dynamic_params: StandardCallbackDynamicParams = ( self.initialize_standard_callback_dynamic_params(kwargs) ) + self._init_kwargs = kwargs # Process dynamic callbacks (after standard_callback_dynamic_params is initialized, # so team-scoped credentials are available for callback initialization) @@ -482,10 +483,12 @@ class Logging(LiteLLMLoggingBaseClass): # pass only the relevant dynamic params as custom_logger_init_args. _custom_logger_init_args: Optional[dict] = None if callback == "datadog": + # dd_* params are blocked from standard_callback_dynamic_params + # (request-level security), but team callback_vars in kwargs + # are admin-configured and trusted. + _source = self._init_kwargs or {} _custom_logger_init_args = { - k: v - for k, v in self.standard_callback_dynamic_params.items() - if k.startswith("dd_") + k: v for k, v in _source.items() if k.startswith("dd_") } callback_class = _init_custom_logger_compatible_class( diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 8cb9a11ffee..767134ef8fa 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -504,10 +504,22 @@ def _get_dynamic_logging_metadata( # Key-based callbacks ######################################################################################### if key_dynamic_logging_settings is not None: + from litellm.litellm_core_utils.initialize_dynamic_callback_params import ( + _request_blocked_callback_params, + ) + for item in key_dynamic_logging_settings: callback = _get_validated_callback_metadata(item=item, source="key-level") if callback is None: continue + # Strip params that could redirect traffic to attacker-controlled + # destinations (e.g. dd_site, dd_agent_host). Key metadata is + # user-configurable; only team-level callbacks are admin-trusted. + callback.callback_vars = { + k: v + for k, v in callback.callback_vars.items() + if k not in _request_blocked_callback_params + } callback_settings_obj = convert_key_logging_metadata_to_callback( data=callback, team_callback_settings_obj=callback_settings_obj, diff --git a/tests/test_litellm/integrations/datadog/test_datadog_team_handler.py b/tests/test_litellm/integrations/datadog/test_datadog_team_handler.py index 1ee497476d1..b70e8d6e129 100644 --- a/tests/test_litellm/integrations/datadog/test_datadog_team_handler.py +++ b/tests/test_litellm/integrations/datadog/test_datadog_team_handler.py @@ -192,3 +192,55 @@ class TestStandardCallbackDynamicParamsIncludesDatadog: assert "dd_site" in annotations assert "dd_agent_host" in annotations assert "dd_agent_port" in annotations + + +class TestTeamCallbackFlowPassesDDCredentials: + """ + Integration test for the full team callback flow. + + Verifies that DD credentials configured via team callback_vars + actually reach DataDogHandler when a request is processed. + The dd_* params are in _request_blocked_callback_params (to prevent + request-level injection), but team callback_vars are admin-configured + and must pass through. + """ + + def test_process_dynamic_callbacks_passes_dd_params_from_kwargs(self): + """ + Simulates the Logging.__init__ flow where team callback_vars + (dd_api_key, dd_site) are unpacked into kwargs. Verifies that + _process_dynamic_callback_list picks them up and passes them + to DataDogHandler despite _request_blocked_callback_params. + """ + from litellm.litellm_core_utils.litellm_logging import Logging + + kwargs = { + "dd_api_key": "team-dd-key-123", + "dd_site": "us5.datadoghq.com", + "model": "gpt-4", + "litellm_params": {"metadata": {}}, + } + + with patch("asyncio.create_task"): + logging_obj = Logging( + model="gpt-4", + messages=[{"role": "user", "content": "hi"}], + stream=False, + call_type="completion", + start_time="2026-01-01", + litellm_call_id="test-call-id", + function_id="test-func", + dynamic_success_callbacks=["datadog"], + kwargs=kwargs, + ) + + dd_loggers = [ + cb + for cb in (logging_obj.dynamic_success_callbacks or []) + if isinstance(cb, DataDogLogger) + ] + assert ( + len(dd_loggers) == 1 + ), "DataDogLogger should be initialized from team callback_vars" + assert dd_loggers[0].DD_API_KEY == "team-dd-key-123" + assert "us5.datadoghq.com" in dd_loggers[0].intake_url