fix(proxy): exclude proxy_server_request from its own body snapshot (#28618)
This commit is contained in:
parent
f25648cdad
commit
9c0a98c9f1
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user