Skip to content

[WIP] Reformat request parameters#26289

Open
Michael-RZ-Berri wants to merge 5 commits intolitellm_internal_stagingfrom
litellm_reformatRequestParameters
Open

[WIP] Reformat request parameters#26289
Michael-RZ-Berri wants to merge 5 commits intolitellm_internal_stagingfrom
litellm_reformatRequestParameters

Conversation

@Michael-RZ-Berri
Copy link
Copy Markdown
Collaborator

Relevant issues

Cleans parameters in requests so functions down the line operate as expected.

This change also disables mock responses by default, users need to re-enable with general_settings.allow_client_side_mock_response: true in config.yaml during development.

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

The attached tests cover the cases where the sanitization is needed, and they pass now.

Type

🐛 Bug Fix
✅ Test

Changes

Importantly, this change turns off mock responses by default so that they can't be used for something similar in production. The mock endpoints need to be re-enabled with general_settings.allow_client_side_mock_response: true in config.yaml, which should be mentioned in the docs.
This touches _read_request_body in http_parsing_utils, also the related test file.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


Michael Riad Zaky seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 23, 2026

Greptile Summary

This PR introduces strip_internal_control_fields — a sanitization layer that removes proxy-internal fields (proxy_server_request, standard_logging_object, mock_response, applied_guardrails, user_api_key_* metadata keys, etc.) from incoming request bodies before any downstream processing sees them. The strip is applied at three ingress points: JSON bodies via _read_request_body, multipart/form bodies via get_form_data, and bracket-notation metadata via extract_nested_form_metadata, closing all three previously-identified bypass paths. The PR also opts the OTEL test config into allow_client_side_mock_response: true to keep that test suite functional.

Confidence Score: 4/5

Safe to merge pending resolution of the backwards-incompatible default noted in a prior review thread.

All three previously-identified P1 bypass paths (JSON ingress, form-data via get_request_body, bracket-notation metadata via extract_nested_form_metadata) are now covered by the sanitization logic. Tests are comprehensive, mock-only, and cover idempotency, unicode round-tripping, opt-in behaviour, and end-to-end cache safety. No new P0/P1 issues were found in this pass. Score is capped at 4 rather than 5 because an unresolved backwards-incompatible default change (flagged in the previous review thread) remains — existing callers sending mock_response in a shared staging proxy will silently hit the live model without any log warning unless ops teams proactively set the flag.

litellm/proxy/common_utils/http_parsing_utils.py — specifically the interaction between _read_request_body's own form-data branch (which does not delegate to get_form_data) and the get_form_data code path, to ensure no duplicate or inconsistent sanitization occurs for the same request across both paths.

Important Files Changed

Filename Overview
litellm/proxy/common_utils/http_parsing_utils.py Adds strip_internal_control_fields and supporting helpers that sanitize proxy-internal fields (mock_response, applied_guardrails, user_api_key_*, etc.) from incoming request bodies at three ingress points: _read_request_body, get_form_data, and extract_nested_form_metadata. JSON-string metadata is parsed, stripped, and re-serialized with ensure_ascii=False.
litellm/proxy/example_config_yaml/otel_test_config.yaml Adds general_settings.allow_client_side_mock_response: true so the existing OTEL test suite (which relies on mock_response via extra_body) continues to work after the new default-off behaviour.
tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py Adds a comprehensive TestStripInternalControlFields suite: covers mock-response default stripping, opt-in preservation, restricted top-level and metadata fields, user_api_key_* prefix stripping, JSON-string metadata sanitisation (including unicode round-trip), idempotency, form-data paths, and the extract_nested_form_metadata bracket-notation path. All tests mock rather than make real network calls, satisfying the repo's test-isolation rule.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming HTTP Request] --> B{Content-Type?}

    B -->|application/json| C[_read_request_body\norjson.loads body]
    B -->|multipart / form-data\napplication/x-www-form-urlencoded| D[get_form_data\nrequest.form]
    B -->|files endpoint\nrequest.form direct| E[extract_nested_form_metadata\nbracket-notation assembly]

    C --> F[strip_internal_control_fields]
    D --> F
    E --> G[_strip_internal_metadata_keys\nmetadata-level only]

    F --> H{_client_mock_response_allowed?}
    H -->|No| I[pop mock_response\npop mock_tool_calls]
    H -->|Yes| J[keep mock fields]
    I --> K[pop _RESTRICTED_TOP_LEVEL_FIELDS]
    J --> K
    K --> L[_strip_internal_metadata_keys\non metadata / litellm_metadata]
    L --> M[strip JSON-string\nmetadata containers]

    M --> N[Cache sanitized body\nrequest.scope.parsed_body]
    G --> O[Return sanitized metadata dict]
    N --> P[Return sanitized body to handler]
Loading

Reviews (6): Last reviewed commit: "extend reformatting to bracket-notation ..." | Re-trigger Greptile

Comment on lines +108 to +110
if not _client_mock_response_allowed():
for key in _MOCK_RESPONSE_FIELDS:
data.pop(key, None)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Backwards-incompatible default change for mock_response

Any client currently sending mock_response in a request body — a common pattern in staging/test environments that point at a shared proxy — will have the field silently dropped and the real LLM called instead. There is no log line at the call site explaining the strip, so the failure mode is opaque: callers see unexpected live-model responses with no indication that their field was ignored.

The opt-in flag exists, but the migration path requires ops teams to discover and set general_settings.allow_client_side_mock_response: true before their test workloads are disrupted. Per the team's rule about backwards-incompatible changes, a safer rollout would be: log a prominent warning when the field is stripped in this release, and default to true (preserve existing behavior) until a future major version flips the default.

if not _client_mock_response_allowed():
    for key in _MOCK_RESPONSE_FIELDS:
        if key in data:
            verbose_proxy_logger.warning(
                "Stripping client-supplied '%s' from request body. "
                "Set general_settings.allow_client_side_mock_response: true "
                "in config.yaml to allow this field.",
                key,
            )
            data.pop(key)

Rule Used: What: avoid backwards-incompatible changes without... (source)

Comment on lines +116 to +122
elif isinstance(container, str):
try:
parsed = json.loads(container)
except (json.JSONDecodeError, ValueError):
continue
if isinstance(parsed, dict) and _strip_internal_metadata_keys(parsed):
data[container_key] = json.dumps(parsed)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 JSON-string metadata: clean string is preserved but dirty one is re-serialized with potential representation drift

When _strip_internal_metadata_keys returns True (something was removed), the sanitized dict is re-serialized with json.dumps. This changes the representation of the original string — key ordering, Unicode escaping, and whitespace may all differ from the client's original. While the data is semantically equivalent, any downstream code that does a raw-string equality check on the metadata value (e.g., a cache key or an HMAC) will see a different string than the client sent.

Consider using ensure_ascii=False to keep the round-trip closer to the original.

@Michael-RZ-Berri Michael-RZ-Berri changed the title Reformat request parameters [WIP] Reformat request parameters Apr 23, 2026
@Michael-RZ-Berri Michael-RZ-Berri force-pushed the litellm_reformatRequestParameters branch from ea70fe7 to 42a2b1f Compare April 25, 2026 16:57
@veria-ai
Copy link
Copy Markdown

veria-ai Bot commented Apr 25, 2026

Medium: Sanitization bypass via multipart/form-data content type

This PR adds ingress sanitization of proxy-internal control fields in _read_request_body, which handles JSON payloads. However, get_form_data() — which handles multipart/form-data requests — does not call strip_internal_control_fields(). Multiple endpoints use get_form_data() directly (audio transcriptions, skills, container uploads, passthrough routes), so an attacker can bypass the new sanitization by sending the same restricted fields via a multipart form request.

The secondary defense in add_litellm_data_to_request() catches some fields (proxy_server_request, standard_logging_object, secret_fields, user_api_key_*, _pipeline_managed_guardrails), but the newly restricted fields (applied_guardrails, applied_policies, policy_sources, pillar_*, _guardrail_pipelines, mock_response, litellm_logging_obj) are not stripped by that secondary layer. Additionally, get_request_body() dispatches to get_form_data() for form content types without sanitization.

  • medium: multipart/form-data bypass of control field sanitization — litellm/proxy/common_utils/http_parsing_utils.py

Status: 0 open
Risk: 4/10

Posted by Veria AI · 2026-04-25T17:36:06.412Z

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 25, 2026

Codecov Report

❌ Patch coverage is 90.24390% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/common_utils/http_parsing_utils.py 90.24% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@veria-ai
Copy link
Copy Markdown

veria-ai Bot commented Apr 25, 2026

All review comments resolved — no active issues remaining.


Status: 0 open
Risk: 0/10

Posted by Veria AI · 2026-04-25T19:40:00.358Z

@veria-ai
Copy link
Copy Markdown

veria-ai Bot commented Apr 25, 2026

Low: Positive security hardening with incomplete endpoint coverage

This PR adds centralized ingress sanitization (strip_internal_control_fields) that strips proxy-internal control fields (guardrail metadata, mock response fields, logging objects, auth metadata prefixes) from user-supplied request bodies at two ingress points: _read_request_body (JSON) and get_form_data (multipart). This is a defense-in-depth improvement over the existing per-function stripping in add_litellm_data_to_request.

The new stripping covers fields not previously stripped at all (applied_guardrails, applied_policies, _guardrail_pipelines, pillar_*, mock_response, mock_tool_calls, litellm_logging_obj). However, many endpoints (evals, moderations, speech, assistants, rerank, search, video, image generation, skills) read the body directly via await request.body() + orjson.loads() and bypass these ingress functions entirely — a gap this PR does not worsen. No new vulnerabilities are introduced.


Status: 0 open
Risk: 2/10

Posted by Veria AI · 2026-04-25T19:43:59.172Z

@veria-ai
Copy link
Copy Markdown

veria-ai Bot commented Apr 25, 2026

Low: Security hardening for request parameter sanitization

This PR adds strip_internal_control_fields at the HTTP ingress layer to remove proxy-internal metadata fields (applied_guardrails, applied_policies, pillar_*, _guardrail_pipelines, etc.) and mock_response/mock_tool_calls from user-supplied request bodies before downstream processing. The stripping is applied in _read_request_body, get_form_data, and extract_nested_form_metadata, covering JSON, multipart, and bracket-notation form paths.

The implementation is sound and well-tested. No new security issues are introduced.


Status: 0 open
Risk: 2/10

Posted by Veria AI · 2026-04-25T19:48:26.781Z


# Strip proxy-internal fields before anything downstream (including
# the cache) sees the body.
strip_internal_control_fields(parsed_body)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Incomplete coverage of ingress sanitization

This strip only runs when the body is parsed through _read_request_body or get_form_data. Five endpoints in proxy_server.py (moderations ~L7857, audio_speech ~L7983, create_assistant ~L8545, add_messages ~L8933, run_thread ~L9126) call orjson.loads(body) directly and then pass data to add_litellm_data_to_request without going through this path. Those endpoints remain vulnerable to injection of fields like applied_guardrails, mock_response, _guardrail_pipelines, etc. that add_litellm_data_to_request doesn't strip.

Consider either calling strip_internal_control_fields(data) inside add_litellm_data_to_request (which all endpoints already use), or refactoring those five endpoints to use _read_request_body.

@veria-ai
Copy link
Copy Markdown

veria-ai Bot commented Apr 25, 2026

Medium: Incomplete ingress sanitization — some endpoints bypass the new strip

This PR adds strip_internal_control_fields at the _read_request_body and get_form_data ingress points, which is a solid hardening measure. However, five endpoints (moderations, audio_speech, create_assistant, add_messages, run_thread) parse the request body directly via orjson.loads(body) instead of using _read_request_body, so they never call strip_internal_control_fields. While add_litellm_data_to_request downstream strips some fields (proxy_server_request, standard_logging_object, secret_fields, user_api_key_*, _pipeline_managed_guardrails), the additional fields covered by the new strip — applied_guardrails, applied_policies, policy_sources, pillar_*, semantic-similarity, _guardrail_pipelines, mock_response, mock_tool_calls, litellm_logging_obj — remain injectable on those endpoints.

  • medium: guardrail/mock_response injection via endpoints that bypass _read_request_body — litellm/proxy/proxy_server.py

Status: 1 new · 1 open
Risk: 4/10

Posted by Veria AI · 2026-04-25T19:50:09.235Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants