From 9a461912f783df42f9a28d405a4857b24fc596d2 Mon Sep 17 00:00:00 2001 From: Michael Riad Zaky Date: Sat, 25 Apr 2026 11:06:37 -0700 Subject: [PATCH] extend reformatting to multipart form ingress --- .../proxy/common_utils/http_parsing_utils.py | 5 ++ .../common_utils/test_http_parsing_utils.py | 57 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/litellm/proxy/common_utils/http_parsing_utils.py b/litellm/proxy/common_utils/http_parsing_utils.py index 36711f44d17..246b6227cc5 100644 --- a/litellm/proxy/common_utils/http_parsing_utils.py +++ b/litellm/proxy/common_utils/http_parsing_utils.py @@ -371,6 +371,11 @@ async def get_form_data(request: Request) -> Dict[str, Any]: parsed_form_data.setdefault(clean_key, []).append(value) else: parsed_form_data[key] = value + # Same ingress sanitization as _read_request_body — multipart endpoints + # (audio transcription, skills, container uploads, passthrough) route + # through here directly via get_request_body and would otherwise bypass + # the strip that JSON ingress applies. + strip_internal_control_fields(parsed_form_data) return parsed_form_data diff --git a/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py b/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py index 21c22a8c055..7387f7cdb11 100644 --- a/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py +++ b/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py @@ -1108,3 +1108,60 @@ class TestStripInternalControlFields: second = await _read_request_body(mock_request) mock_request.body.assert_not_called() assert second["metadata"]["user_api_key_user_id"] == "real-user" + + @pytest.mark.asyncio + async def test_get_form_data_strips_restricted_fields(self, monkeypatch): + """Multipart endpoints (audio transcription, skills, container + uploads, passthrough) route through get_form_data directly via + get_request_body and would otherwise bypass the JSON-ingress + strip. Form values are strings, so metadata arrives as a JSON + string.""" + self._clear_allow_mock(monkeypatch) + mock_request = MagicMock() + mock_request.form = AsyncMock( + return_value={ + "model": "whisper-1", + "file": "", + "mock_response": "canned", + "metadata": json.dumps( + { + "applied_guardrails": ["caller"], + "pillar_response_headers": {"X-Caller": "yes"}, + "user_api_key_user_id": "caller-supplied", + "tag": "ok", + } + ), + } + ) + + result = await get_form_data(mock_request) + + assert "mock_response" not in result + # metadata stays a string (form values are strings) but the + # restricted keys inside it are gone. + assert isinstance(result["metadata"], str) + assert json.loads(result["metadata"]) == {"tag": "ok"} + # Other form fields are untouched. + assert result["model"] == "whisper-1" + assert result["file"] == "" + + @pytest.mark.asyncio + async def test_get_request_body_dispatcher_strips_multipart( + self, monkeypatch + ): + """The dispatcher routes multipart/form-data to get_form_data; + confirm that path is also sanitized end-to-end.""" + self._clear_allow_mock(monkeypatch) + mock_request = MagicMock() + mock_request.method = "POST" + mock_request.headers = {"content-type": "multipart/form-data; boundary=x"} + mock_request.form = AsyncMock( + return_value={ + "model": "whisper-1", + "mock_response": "canned", + } + ) + + result = await get_request_body(mock_request) + assert "mock_response" not in result + assert result["model"] == "whisper-1"