From 703eb4fa6871feac8fc4de6260e9e0faa21a343e Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:16:58 -0700 Subject: [PATCH] security(proxy): keep team callback credentials out of the stored request body (#43217) * security(proxy): keep team callback credentials out of the stored request body Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ci: allow the security conventional commit type Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: oliver Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .githooks/commit-msg | 4 +-- .github/workflows/conventional-commits.yml | 1 + litellm/proxy/litellm_pre_call_utils.py | 7 +++++- .../proxy/test_litellm_pre_call_utils.py | 25 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.githooks/commit-msg b/.githooks/commit-msg index b64e38a2286..b602861c672 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -42,7 +42,7 @@ case "$subject" in ;; esac -ALLOWED_TYPES="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert" +ALLOWED_TYPES="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|security" # Description must not start with an uppercase letter — kept in sync with the # subjectPattern in .github/workflows/conventional-commits.yml so the local # hook is the strictly tighter of the two gates. (Without this guard, a commit @@ -61,7 +61,7 @@ cat >&2 <()!: (description must start with a lowercase letter) - Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, security Examples: feat(router): add weighted round-robin strategy fix(bedrock): decouple STS region from aws_region_name diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index eb9eb69f8b6..4ae59ed581f 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -41,6 +41,7 @@ jobs: ci chore revert + security requireScope: false subjectPattern: ^(?![A-Z]).+$ subjectPatternError: | diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 866d84ca8f2..d48451de6b1 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -196,6 +196,9 @@ from litellm.types.utils import ( SupportedCacheControls, ) +_CALLBACK_CREDENTIAL_KEYS: Final = frozenset(StandardCallbackDynamicParams.__annotations__) | frozenset( + {TRUSTED_CALLBACK_VARS_FIELD} +) service_logger_obj: Final = ServiceLogging() # used for tracking latency on OTEL # Bounded dedup for stale-alias warnings (FIFO eviction when over cap). _MAX_STALE_ALIAS_WARNING_KEYS: Final = 10_000 @@ -1958,7 +1961,9 @@ def refresh_proxy_server_request_body_snapshot( if not isinstance(proxy_server_request, dict): return _body_snapshot_exclude: Final = ( - frozenset({"secret_fields", "proxy_server_request", "litellm_logging_obj"}) | _TRANSPORT_ONLY_CREDENTIAL_KEYS + frozenset({"secret_fields", "proxy_server_request", "litellm_logging_obj"}) + | _TRANSPORT_ONLY_CREDENTIAL_KEYS + | _CALLBACK_CREDENTIAL_KEYS ) body: Final = { # mutable-ok: audit JSON serialization requires a dict with shared nested messages k: v for k, v in data.items() if k not in _body_snapshot_exclude diff --git a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py index 1a3ffecb0a5..84266325226 100644 --- a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py +++ b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py @@ -870,6 +870,31 @@ def test_initial_snapshot_refresh_clears_a_previous_guardrail_checkpoint() -> No assert proxy_request == {"body": {"messages": [{"role": "user", "content": "new request"}]}} +def test_body_snapshot_excludes_team_callback_credentials() -> None: + from litellm.proxy.litellm_pre_call_utils import refresh_proxy_server_request_body_snapshot + from litellm.types.litellm_params import TRUSTED_CALLBACK_VARS_FIELD + + callback_vars: Final = { + "langfuse_public_key": "pk-lf-team", + "langfuse_secret_key": "sk-lf-team-secret", + "langfuse_host": "https://cloud.langfuse.com", + } + proxy_request: Final = {"body": None} + data: Final = { + "messages": [{"role": "user", "content": "hi"}], + "proxy_server_request": proxy_request, + "success_callback": ["langfuse"], + **callback_vars, + TRUSTED_CALLBACK_VARS_FIELD: callback_vars, + } + + refresh_proxy_server_request_body_snapshot(data) + + assert proxy_request == { + "body": {"messages": [{"role": "user", "content": "hi"}], "success_callback": ["langfuse"]} + }, proxy_request + + @pytest.mark.asyncio @pytest.mark.parametrize("pre_call_ran", [False, True]) async def test_post_guardrail_snapshot_preserves_logging_only_masking_in_spend_logs(