fix: prevent path traversal via model name in Azure deployment URLs (#23629)

The model name from user input was interpolated directly into Azure deployment URL paths without validation. A user could send a model name like '../../management/foo' to traverse the URL path and hit unintended Azure endpoints with the admin's API key.

Adds _sanitize_model_for_url that rejects path separators and traversal sequences, and percent-encodes the name. Applied at convert_to_azure_payload (covers chat completions + proxy) and the responses endpoint's direct URL construction.
This commit is contained in:
Classic298
2026-04-12 12:29:45 -05:00
committed by GitHub
parent 4498c21f4c
commit a2a9a3a42a
+24 -2
View File
@@ -4,7 +4,7 @@ import json
import logging import logging
import re import re
from typing import Optional from typing import Optional
from urllib.parse import urlparse from urllib.parse import quote, urlparse
import aiohttp import aiohttp
from aiocache import cached from aiocache import cached
@@ -772,6 +772,21 @@ def is_openai_new_model(model: str) -> bool:
return False return False
def _sanitize_model_for_url(model: str) -> str:
"""Sanitize a model name before interpolating it into a URL path.
Rejects path traversal attempts (../, /, \\) and percent-encodes
the name so it is safe to use as a single URL path segment
(e.g. Azure deployment name).
"""
if not model or '..' in model or '/' in model or '\\' in model:
raise HTTPException(
status_code=400,
detail='Invalid model name: must not be empty or contain path separators or traversal sequences',
)
return quote(model, safe='')
def convert_to_azure_payload(url, payload: dict, api_version: str): def convert_to_azure_payload(url, payload: dict, api_version: str):
model = payload.get('model', '') model = payload.get('model', '')
@@ -795,6 +810,9 @@ def convert_to_azure_payload(url, payload: dict, api_version: str):
# Filter out unsupported parameters # Filter out unsupported parameters
payload = {k: v for k, v in payload.items() if k in allowed_params} payload = {k: v for k, v in payload.items() if k in allowed_params}
# Sanitize model name to prevent path traversal in the deployment URL
model = _sanitize_model_for_url(model)
url = f'{url}/openai/deployments/{model}' url = f'{url}/openai/deployments/{model}'
return url, payload return url, payload
@@ -1364,7 +1382,7 @@ async def responses(
else: else:
api_version = api_config.get('api_version', '2023-03-15-preview') api_version = api_config.get('api_version', '2023-03-15-preview')
headers['api-version'] = api_version headers['api-version'] = api_version
model = payload.get('model', '') model = _sanitize_model_for_url(payload.get('model', ''))
request_url = f'{url}/openai/deployments/{model}/responses?api-version={api_version}' request_url = f'{url}/openai/deployments/{model}/responses?api-version={api_version}'
else: else:
request_url = f'{url}/responses' request_url = f'{url}/responses'
@@ -1404,6 +1422,8 @@ async def responses(
return response_data return response_data
except HTTPException:
raise
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
raise HTTPException( raise HTTPException(
@@ -1515,6 +1535,8 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
return response_data return response_data
except HTTPException:
raise
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
raise HTTPException( raise HTTPException(