mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
fix: harden pass-through target URL construction
Use posixpath-based joining when combining the operator-configured base target with request subpaths and provider endpoint paths so any path prefix on the base URL is preserved and '..' segments cannot climb out of it. Adds a shared HttpPassThroughEndpointHelpers.join_base_and_endpoint_path helper and routes the generic and provider pass-through handlers (Anthropic, Gemini, Mistral, Cohere, Cursor, Milvus, OpenAI, etc.) through it.
This commit is contained in:
parent
9521b74e9a
commit
961672efa2
2 changed files with 100 additions and 37 deletions
|
|
@ -127,18 +127,14 @@ async def llm_passthrough_factory_proxy_route(
|
|||
if not encoded_endpoint.startswith("/"):
|
||||
encoded_endpoint = "/" + encoded_endpoint
|
||||
|
||||
# Construct the full target URL using httpx
|
||||
# Construct the full target URL using httpx, preserving any base path
|
||||
# prefix that the operator configured on base_target_url.
|
||||
base_url = httpx.URL(base_target_url)
|
||||
# Join paths correctly by removing trailing/leading slashes as needed
|
||||
if not base_url.path or base_url.path == "/":
|
||||
# If base URL has no path, just use the new path
|
||||
updated_url = base_url.copy_with(path=encoded_endpoint)
|
||||
else:
|
||||
# Otherwise, combine the paths
|
||||
base_path = base_url.path.rstrip("/")
|
||||
clean_path = encoded_endpoint.lstrip("/")
|
||||
full_path = f"{base_path}/{clean_path}"
|
||||
updated_url = base_url.copy_with(path=full_path)
|
||||
updated_url = base_url.copy_with(
|
||||
path=HttpPassThroughEndpointHelpers.join_base_and_endpoint_path(
|
||||
base_url, encoded_endpoint
|
||||
)
|
||||
)
|
||||
|
||||
# Add or update query parameters
|
||||
provider_api_key = passthrough_endpoint_router.get_credentials(
|
||||
|
|
@ -215,9 +211,14 @@ async def gemini_proxy_route(
|
|||
if not encoded_endpoint.startswith("/"):
|
||||
encoded_endpoint = "/" + encoded_endpoint
|
||||
|
||||
# Construct the full target URL using httpx
|
||||
# Construct the full target URL using httpx, preserving any base path
|
||||
# prefix that the operator configured on base_target_url.
|
||||
base_url = httpx.URL(base_target_url)
|
||||
updated_url = base_url.copy_with(path=encoded_endpoint)
|
||||
updated_url = base_url.copy_with(
|
||||
path=HttpPassThroughEndpointHelpers.join_base_and_endpoint_path(
|
||||
base_url, encoded_endpoint
|
||||
)
|
||||
)
|
||||
|
||||
# Add or update query parameters
|
||||
gemini_api_key: Optional[str] = passthrough_endpoint_router.get_credentials(
|
||||
|
|
@ -275,9 +276,14 @@ async def cohere_proxy_route(
|
|||
if not encoded_endpoint.startswith("/"):
|
||||
encoded_endpoint = "/" + encoded_endpoint
|
||||
|
||||
# Construct the full target URL using httpx
|
||||
# Construct the full target URL using httpx, preserving any base path
|
||||
# prefix that the operator configured on base_target_url.
|
||||
base_url = httpx.URL(base_target_url)
|
||||
updated_url = base_url.copy_with(path=encoded_endpoint)
|
||||
updated_url = base_url.copy_with(
|
||||
path=HttpPassThroughEndpointHelpers.join_base_and_endpoint_path(
|
||||
base_url, encoded_endpoint
|
||||
)
|
||||
)
|
||||
|
||||
# Add or update query parameters
|
||||
cohere_api_key = passthrough_endpoint_router.get_credentials(
|
||||
|
|
@ -401,9 +407,14 @@ async def mistral_proxy_route(
|
|||
if not encoded_endpoint.startswith("/"):
|
||||
encoded_endpoint = "/" + encoded_endpoint
|
||||
|
||||
# Construct the full target URL using httpx
|
||||
# Construct the full target URL using httpx, preserving any base path
|
||||
# prefix that the operator configured on base_target_url.
|
||||
base_url = httpx.URL(base_target_url)
|
||||
updated_url = base_url.copy_with(path=encoded_endpoint)
|
||||
updated_url = base_url.copy_with(
|
||||
path=HttpPassThroughEndpointHelpers.join_base_and_endpoint_path(
|
||||
base_url, encoded_endpoint
|
||||
)
|
||||
)
|
||||
|
||||
# Add or update query parameters
|
||||
mistral_api_key = passthrough_endpoint_router.get_credentials(
|
||||
|
|
@ -546,9 +557,14 @@ async def milvus_proxy_route(
|
|||
if not encoded_endpoint.startswith("/"):
|
||||
encoded_endpoint = "/" + encoded_endpoint
|
||||
|
||||
# Construct the full target URL using httpx
|
||||
# Construct the full target URL using httpx, preserving any base path
|
||||
# prefix that the operator configured on base_target_url.
|
||||
base_url = httpx.URL(base_target_url)
|
||||
updated_url = base_url.copy_with(path=encoded_endpoint)
|
||||
updated_url = base_url.copy_with(
|
||||
path=HttpPassThroughEndpointHelpers.join_base_and_endpoint_path(
|
||||
base_url, encoded_endpoint
|
||||
)
|
||||
)
|
||||
## CREATE PASS-THROUGH
|
||||
endpoint_func = create_pass_through_route(
|
||||
endpoint=endpoint,
|
||||
|
|
@ -601,9 +617,14 @@ async def anthropic_proxy_route(
|
|||
if not encoded_endpoint.startswith("/"):
|
||||
encoded_endpoint = "/" + encoded_endpoint
|
||||
|
||||
# Construct the full target URL using httpx
|
||||
# Construct the full target URL using httpx, preserving any base path
|
||||
# prefix that the operator configured on base_target_url.
|
||||
base_url = httpx.URL(base_target_url)
|
||||
updated_url = base_url.copy_with(path=encoded_endpoint)
|
||||
updated_url = base_url.copy_with(
|
||||
path=HttpPassThroughEndpointHelpers.join_base_and_endpoint_path(
|
||||
base_url, encoded_endpoint
|
||||
)
|
||||
)
|
||||
|
||||
# Add or update query parameters
|
||||
anthropic_api_key = passthrough_endpoint_router.get_credentials(
|
||||
|
|
@ -1055,9 +1076,14 @@ async def bedrock_proxy_route(
|
|||
if not encoded_endpoint.startswith("/"):
|
||||
encoded_endpoint = "/" + encoded_endpoint
|
||||
|
||||
# Construct the full target URL using httpx
|
||||
# Construct the full target URL using httpx, preserving any base path
|
||||
# prefix that the operator configured on base_target_url.
|
||||
base_url = httpx.URL(base_target_url)
|
||||
updated_url = base_url.copy_with(path=encoded_endpoint)
|
||||
updated_url = base_url.copy_with(
|
||||
path=HttpPassThroughEndpointHelpers.join_base_and_endpoint_path(
|
||||
base_url, encoded_endpoint
|
||||
)
|
||||
)
|
||||
|
||||
# Add or update query parameters
|
||||
from litellm.llms.bedrock.chat import BedrockConverseLLM
|
||||
|
|
@ -1231,9 +1257,14 @@ async def assemblyai_proxy_route(
|
|||
if not encoded_endpoint.startswith("/"):
|
||||
encoded_endpoint = "/" + encoded_endpoint
|
||||
|
||||
# Construct the full target URL using httpx
|
||||
# Construct the full target URL using httpx, preserving any base path
|
||||
# prefix that the operator configured on base_target_url.
|
||||
base_url = httpx.URL(base_target_url)
|
||||
updated_url = base_url.copy_with(path=encoded_endpoint)
|
||||
updated_url = base_url.copy_with(
|
||||
path=HttpPassThroughEndpointHelpers.join_base_and_endpoint_path(
|
||||
base_url, encoded_endpoint
|
||||
)
|
||||
)
|
||||
|
||||
# Add or update query parameters
|
||||
assemblyai_api_key = passthrough_endpoint_router.get_credentials(
|
||||
|
|
@ -2086,16 +2117,15 @@ class BaseOpenAIPassThroughHandler:
|
|||
"""
|
||||
Properly joins a base URL with a path, preserving any existing path in the base URL.
|
||||
"""
|
||||
# Join paths correctly by removing trailing/leading slashes as needed
|
||||
if not base_url.path or base_url.path == "/":
|
||||
# If base URL has no path, just use the new path
|
||||
joined_path_str = str(base_url.copy_with(path=path))
|
||||
else:
|
||||
# Otherwise, combine the paths
|
||||
base_path = base_url.path.rstrip("/")
|
||||
clean_path = path.lstrip("/")
|
||||
full_path = f"{base_path}/{clean_path}"
|
||||
joined_path_str = str(base_url.copy_with(path=full_path))
|
||||
# Combine paths via the shared helper so any '..' in the path cannot
|
||||
# climb above the configured base path.
|
||||
joined_path_str = str(
|
||||
base_url.copy_with(
|
||||
path=HttpPassThroughEndpointHelpers.join_base_and_endpoint_path(
|
||||
base_url, path
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Apply OpenAI-specific path handling for both branches
|
||||
if (
|
||||
|
|
@ -2176,7 +2206,11 @@ async def cursor_proxy_route(
|
|||
encoded_endpoint = "/" + encoded_endpoint
|
||||
|
||||
base_url = httpx.URL(base_target_url)
|
||||
updated_url = base_url.copy_with(path=encoded_endpoint)
|
||||
updated_url = base_url.copy_with(
|
||||
path=HttpPassThroughEndpointHelpers.join_base_and_endpoint_path(
|
||||
base_url, encoded_endpoint
|
||||
)
|
||||
)
|
||||
|
||||
auth_value = base64.b64encode(f"{cursor_api_key}:".encode("utf-8")).decode("ascii")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import ast
|
|||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import posixpath
|
||||
import traceback
|
||||
from base64 import b64encode
|
||||
from datetime import datetime
|
||||
|
|
@ -599,7 +600,35 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils):
|
|||
if subpath.startswith("/"):
|
||||
subpath = subpath[1:]
|
||||
|
||||
return base_target + subpath
|
||||
# Resolve any '..' segments in the subpath so it cannot climb above
|
||||
# the base_target prefix that the operator configured.
|
||||
safe_subpath = posixpath.normpath("/" + subpath).lstrip("/")
|
||||
if safe_subpath == ".":
|
||||
safe_subpath = ""
|
||||
|
||||
return base_target + safe_subpath
|
||||
|
||||
@staticmethod
|
||||
def join_base_and_endpoint_path(base_url: httpx.URL, endpoint_path: str) -> str:
|
||||
"""
|
||||
Combine the path component of ``base_url`` with ``endpoint_path``.
|
||||
|
||||
Preserves any path prefix configured on the base URL and resolves
|
||||
``..`` segments in the endpoint so the result stays within the base
|
||||
path.
|
||||
"""
|
||||
base_path = base_url.path or ""
|
||||
if not base_path or base_path == "/":
|
||||
normalized_endpoint = posixpath.normpath("/" + endpoint_path.lstrip("/"))
|
||||
return normalized_endpoint
|
||||
|
||||
base_path = base_path.rstrip("/")
|
||||
clean_endpoint = endpoint_path.lstrip("/")
|
||||
combined = posixpath.normpath(base_path + "/" + clean_endpoint)
|
||||
# If normalization climbs out of the base path, fall back to base.
|
||||
if combined != base_path and not combined.startswith(base_path + "/"):
|
||||
return base_path + "/"
|
||||
return combined
|
||||
|
||||
@staticmethod
|
||||
def _update_stream_param_based_on_request_body(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue