Skip to content

chore(proxy): cherry-pick #28547 onto patch/v1.86.1 - #28969

Merged
yuneng-berri merged 2 commits into
patch/v1.86.1from
litellm_yj/cherry-pick-28547-v1.86.1
May 27, 2026
Merged

chore(proxy): cherry-pick #28547 onto patch/v1.86.1#28969
yuneng-berri merged 2 commits into
patch/v1.86.1from
litellm_yj/cherry-pick-28547-v1.86.1

Conversation

@yuneng-berri

Copy link
Copy Markdown
Collaborator

Backport of #28547 (d480ffda3c) onto patch/v1.86.1.

Routes the remaining path-dependent call sites in auth, ACL, routing, and audit-log decisions through get_request_route(request) so they read from the ASGI scope["path"] instead of request.url.path. The helper itself already exists on v1.86.1 (added by #27878); this PR extends the helper's usage to the additional sites listed below.

Sites routed through get_request_route

  • _experimental/mcp_server/auth/user_api_key_auth_mcp.py
  • management_endpoints/mcp_management_endpoints.py
  • vector_store_endpoints/utils.py
  • pass_through_endpoints/pass_through_endpoints.py
  • auth/route_checks.py
  • litellm_pre_call_utils.py
  • spend_tracking/spend_management_endpoints.py
  • common_utils/http_parsing_utils.py
  • management_helpers/utils.py
  • health_endpoints/_health_endpoints.py

Conflict resolution

Cherry-pick applied cleanly with no conflicts. All 11 files plus the test file are pure request.url.pathget_request_route(request) swaps with the lazy auth_utils import (no feature drift).

Test plan

  • uv run pytest tests/proxy_unit_tests/test_proxy_routes.py -v
  • make test-unit

Backport of #28547 (`d480ffda3c`) onto the `patch/v1.86.1` branch.

Routes the remaining path-dependent call sites in auth, ACL, routing,
and audit-log decisions through `get_request_route(request)` so they
read from the ASGI `scope["path"]` instead of `request.url.path`. The
helper itself already exists on v1.86.1 (added by #27878); this PR
extends the helper's usage to the additional sites listed below.

Sites routed through get_request_route:
- _experimental/mcp_server/auth/user_api_key_auth_mcp.py
- management_endpoints/mcp_management_endpoints.py
- vector_store_endpoints/utils.py
- pass_through_endpoints/pass_through_endpoints.py
- auth/route_checks.py
- litellm_pre_call_utils.py
- spend_tracking/spend_management_endpoints.py
- common_utils/http_parsing_utils.py
- management_helpers/utils.py
- health_endpoints/_health_endpoints.py

Regression tests in tests/proxy_unit_tests/test_proxy_routes.py
construct a Request with scope["path"] set to a benign route and the
Host header crafted so url.path would resolve differently; each
site's decision is asserted against scope["path"].

Conflict resolution
-------------------

Cherry-pick applied cleanly with no conflicts. All 11 files plus the
test file are pure `request.url.path` → `get_request_route(request)`
swaps with the lazy auth_utils import (no feature drift).
@greptile-apps

greptile-apps Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This cherry-pick backports #28547 onto patch/v1.86.1, extending the existing get_request_route helper to all remaining proxy call sites that previously read request.url.path directly. The motivation is a Host-header injection vector: Starlette reconstructs url.path by embedding the Host header into a URL string and re-parsing with urlsplit, so a malformed Host (e.g. localhost/?x=1) can collapse url.path to / or an attacker-controlled value while FastAPI still dispatches on scope[\"path\"].

  • All 10 production call sites across auth, ACL, routing, MCP, spend-log, and audit-log are migrated to get_request_route; the helper already existed and has a correct fallback.
  • The new parametric test suite in test_proxy_routes.py covers 7 of the changed call sites (35 combinations of host × path) and validates that scope path is authoritative; the vector store ACL functions (is_allowed_to_call_vector_store_endpoint, is_allowed_to_call_vector_store_files_endpoint) are migrated correctly but lack a dedicated bypass regression test.

Confidence Score: 4/5

The auth and ACL fixes are correct and consistent across all changed files; the cherry-pick is clean with no feature drift from the original PR.

All 10 production sites are correctly migrated to get_request_route, the helper's fallback logic is sound, and the new test suite validates the most security-sensitive paths (MCP auth bypass, assistants classification, spend-log routing, health echo). The only gap is that is_allowed_to_call_vector_store_endpoint and is_allowed_to_call_vector_store_files_endpoint — both ACL decision points — have no parametric test in the new suite to confirm the bypass is closed.

tests/proxy_unit_tests/test_proxy_routes.py — missing a _CALL_SITES entry for the vector store ACL bypass; litellm/proxy/vector_store_endpoints/utils.py is otherwise correct.

Important Files Changed

Filename Overview
litellm/proxy/auth/auth_utils.py Docstring updated to explain the security motivation for using scope["path"] over request.url.path; no functional logic change.
litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py Routes all three path-dependent checks (/.well-known/ bypass, upstream-delegate, OAuth2 mode) through get_request_route; inline import avoids import cycle.
litellm/proxy/auth/route_checks.py _is_assistants_api_request now reads the route from scope instead of url.path, preventing thread/assistant substring injection via Host header.
litellm/proxy/management_endpoints/mcp_management_endpoints.py PKCE /token suffix check migrated to get_request_route; the original or "" guard on url.path was unnecessary since the helper always returns str.
litellm/proxy/vector_store_endpoints/utils.py ACL permission checks (read/write) for vector store endpoints now use scope path; the new test suite has no direct parametric test for is_allowed_to_call_vector_store_endpoint bypass.
litellm/proxy/common_utils/http_parsing_utils.py _add_vector_store_id_from_path now extracts the vector_store_id from the scope path rather than url.path.
litellm/proxy/litellm_pre_call_utils.py _get_metadata_variable_name uses scope path for thread/assistant detection; covered by a parametric regression test.
litellm/proxy/management_helpers/utils.py Both OTel route-tagging sites inside management_endpoint_wrapper now use get_request_route; no test for these but they are non-security audit-log paths.
litellm/proxy/pass_through_endpoints/pass_through_endpoints.py Pass-through path determination uses get_request_route, which also strips root_path; change is consistent with the rest of the proxy.
litellm/proxy/spend_tracking/spend_management_endpoints.py v2 spend-log classification uses scope path; tested by spend_logs_v2_classification parametric case.
litellm/proxy/health_endpoints/_health_endpoints.py test_endpoint echo now returns scope path; covered by health_route_echo parametric test.
tests/proxy_unit_tests/test_proxy_routes.py New parametric test suite covers 7 call sites against 4 bypass host patterns; vector store ACL (is_allowed_to_call_vector_store_endpoint) and pass-through path are not covered by a direct parametric test.

Reviews (1): Last reviewed commit: "chore(proxy): cherry-pick #28547 onto pa..." | Re-trigger Greptile

Comment on lines +280 to +315
_CALL_SITES = [
("assistants_classification", "/key/generate", "%s/thread", _is_assistants, False),
(
"metadata_variable_name",
"/chat/completions",
"%s/thread",
_metadata_var_name,
"metadata",
),
(
"vector_store_id_extraction",
"/key/generate",
"%s/vector_stores/x/files",
_vector_store_id_in_path,
False,
),
(
"well_known_mcp_bypass",
"/mcp/tools/call",
"/.well-known/%s",
lambda r: get_request_route(r).startswith("/.well-known/"),
False,
),
(
"pkce_token_suffix",
"/mcp/server-id/token",
"%s",
lambda r: get_request_route(r).rstrip("/").lower().endswith("/token"),
True,
),
(
"spend_logs_v2_classification",
"/spend/logs",
"%s/spend/logs/v2",
lambda r: "/spend/logs/v2" in get_request_route(r),
False,

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 Missing direct test coverage for vector store ACL bypass

is_allowed_to_call_vector_store_endpoint and is_allowed_to_call_vector_store_files_endpoint in vector_store_endpoints/utils.py are security-relevant permission checks (read/write ACL decisions) that were migrated to get_request_route, but no entry in _CALL_SITES exercises them. A crafted Host header that injects a read-matching endpoint pattern into url.path while scope["path"] is an unrelated route would have previously bypassed the write guard; the fix is correct but the parametric suite doesn't confirm it. Adding a case analogous to vector_store_id_extraction would close the gap.

@codecov

codecov Bot commented May 27, 2026

Copy link
Copy Markdown

@yuneng-berri
yuneng-berri merged commit be557c8 into patch/v1.86.1 May 27, 2026
25 of 48 checks passed
@yuneng-berri yuneng-berri mentioned this pull request May 27, 2026
1 task
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.

1 participant