fix(proxy): exclude proxy_server_request from its own body snapshot (#28618)

This commit is contained in:
michelligabriele 2026-05-26 23:16:12 +02:00 committed by GitHub
parent f25648cdad
commit 9c0a98c9f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 63 additions and 4 deletions

View File

@ -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.

View File

@ -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