From 9c0a98c9f107c653e82cb59212ab1a17dfbdf84b Mon Sep 17 00:00:00 2001 From: michelligabriele Date: Tue, 26 May 2026 23:16:12 +0200 Subject: [PATCH] fix(proxy): exclude proxy_server_request from its own body snapshot (#28618) --- litellm/proxy/litellm_pre_call_utils.py | 14 +++-- .../proxy/test_litellm_pre_call_utils.py | 53 +++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 0d27b283c4..ca860fd95f 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -1513,10 +1513,16 @@ async def add_litellm_data_to_request( # noqa: PLR0915 # spend_tracking_utils, streaming_iterator) read `body` to audit the # request; taking the snapshot here ensures they see cleaned metadata. # - # Exclude secret_fields (which contains raw_headers with Authorization - # tokens) from the snapshot — they must never be persisted in spend logs - # or any other audit trail. - _body_snapshot = {k: v for k, v in data.items() if k != "secret_fields"} + # Exclude: + # - secret_fields: contains raw_headers with Authorization tokens; must + # never be persisted in spend logs or any other audit trail. + # - proxy_server_request: already a key on `data` at this point (set + # earlier in this function); including it would make the snapshot + # self-reference — body.proxy_server_request.body would be the same + # dict as body, producing an infinite traversal loop for any consumer + # that walks the structure. + _body_snapshot_exclude = {"secret_fields", "proxy_server_request"} + _body_snapshot = {k: v for k, v in data.items() if k not in _body_snapshot_exclude} data["proxy_server_request"]["body"] = _body_snapshot # Snapshot the requester-supplied metadata for downstream consumers. 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 fc9813ba53..26bbbfecfd 100644 --- a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py +++ b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py @@ -515,6 +515,59 @@ async def test_add_litellm_data_to_request_body_snapshot_excludes_secret_fields( ) +@pytest.mark.asyncio +async def test_add_litellm_data_to_request_body_snapshot_excludes_proxy_server_request(): + """Regression: the body snapshot used to include the proxy_server_request + key itself, producing the path + ``proxy_server_request.body.proxy_server_request.body == body``. Custom + loggers and audit consumers must not see the self-referencing structure + (independent of redaction — fires on every successful call). + """ + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request + + request_mock = MagicMock(spec=Request) + request_mock.url.path = "/v1/chat/completions" + request_mock.url = MagicMock() + request_mock.url.__str__.return_value = "http://localhost/v1/chat/completions" + request_mock.method = "POST" + request_mock.query_params = {} + request_mock.headers = {"Content-Type": "application/json"} + request_mock.client = MagicMock() + request_mock.client.host = "127.0.0.1" + + data = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "hello"}], + } + + user_api_key_dict = UserAPIKeyAuth( + api_key="hashed-key", + user_id="test-user", + metadata={}, + team_metadata={}, + spend=0.0, + max_budget=100.0, + model_max_budget={}, + team_spend=0.0, + team_max_budget=200.0, + ) + + updated = await add_litellm_data_to_request( + data=data, + request=request_mock, + user_api_key_dict=user_api_key_dict, + proxy_config=MagicMock(), + general_settings={}, + version="test-version", + ) + + snapshot_body = updated["proxy_server_request"]["body"] + assert "proxy_server_request" not in snapshot_body, ( + "proxy_server_request must be excluded from its own body snapshot " + "to prevent the body from self-referencing" + ) + + @pytest.mark.asyncio async def test_add_litellm_data_to_request_strips_string_encoded_admin_injection(): """Regression: metadata arriving as a JSON string (multipart/form-data or