fix(batches): mask pre-signed request auth headers before raw-request logging

A pre-signed batch/file request (Mistral, Bedrock) carries its auth header
inside the transformed request body, which pre_call logs verbatim into
raw_request_typed_dict and raw-request callbacks, leaking the provider key.
Mask the nested headers channel before handing the request to pre_call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mubashir1osmani 2026-09-18 22:50:19 -04:00
parent 4e4008cea9
commit 8cf2606e2d
2 changed files with 82 additions and 3 deletions

View file

@ -278,6 +278,23 @@ def _has_pre_call_deployment_hook(logging_obj: LiteLLMLoggingObj) -> bool:
return False
def _mask_presigned_request_headers(transformed_request: bytes | str | dict) -> bytes | str | dict:
"""A pre-signed request carries its auth inside its own ``headers`` key, which
logging treats as request body (only the top-level headers channel gets masked),
so mask it here before the request is handed to ``pre_call``."""
if not isinstance(transformed_request, dict):
return transformed_request
request_headers: Final = transformed_request.get("headers")
if not isinstance(request_headers, dict):
return transformed_request
from litellm.litellm_core_utils.litellm_logging import (
_get_masked_values, # pyright: ignore[reportPrivateUsage] # the shared header-masking helper has no public name
)
return {**transformed_request, "headers": _get_masked_values(request_headers)}
def _aws_signing_overrides(optional_params: Mapping[str, Any], litellm_params: Mapping[str, Any]) -> Mapping[str, Any]:
return MappingProxyType(
{
@ -3692,7 +3709,7 @@ class BaseLLMHTTPHandler:
"complete_input_dict": (
"<streaming media upload>"
if isinstance(transformed_request, dict) and "streaming_media_upload" in transformed_request
else transformed_request
else _mask_presigned_request_headers(transformed_request)
),
"api_base": api_base,
"headers": headers,
@ -4115,7 +4132,7 @@ class BaseLLMHTTPHandler:
input="",
api_key="",
additional_args={
"complete_input_dict": transformed_request,
"complete_input_dict": _mask_presigned_request_headers(transformed_request),
"api_base": api_base,
"headers": headers,
},
@ -4194,7 +4211,7 @@ class BaseLLMHTTPHandler:
input="",
api_key="",
additional_args={
"complete_input_dict": transformed_request,
"complete_input_dict": _mask_presigned_request_headers(transformed_request),
"api_base": api_base,
"headers": headers,
"batch_id": batch_id,

View file

@ -2761,6 +2761,68 @@ def test_direct_vector_store_search_debug_log_omits_stored_credentials(caplog, i
assert "sk-embedding-s3cret" not in logged
@pytest.mark.asyncio
async def test_async_retrieve_batch_masks_presigned_auth_header_in_raw_request_log():
"""Regression: a pre-signed retrieve-batch request (Mistral, Bedrock) embeds its auth
header inside the transformed request, which pre_call logs verbatim as the raw request
body, so the provider key landed unmasked in raw_request_typed_dict and every
raw-request callback."""
from litellm.litellm_core_utils.litellm_logging import Logging as LitellmLogging
from litellm.llms.mistral.batches.transformation import MistralBatchesConfig
provider_key = "mistral-s3cret-provider-key-123456"
job_payload = {
"id": "batch-1",
"input_files": ["file-1"],
"endpoint": "/v1/ocr",
"model": "mistral-ocr-latest",
"status": "SUCCESS",
"created_at": 1_757_400_000,
}
sent_requests = []
def _capture(request: httpx.Request) -> httpx.Response:
sent_requests.append(request)
return httpx.Response(200, json=job_payload)
client = AsyncHTTPHandler()
client.client = httpx.AsyncClient(transport=httpx.MockTransport(_capture))
logging_obj = LitellmLogging(
model="mistral/mistral-ocr-latest",
messages=[],
stream=False,
call_type="batch_retrieve",
start_time=time.time(),
litellm_call_id="batch-retrieve-call-id",
function_id="batch-retrieve-function-id",
log_raw_request_response=True,
)
logging_obj.update_environment_variables(
model="mistral/mistral-ocr-latest",
optional_params={},
litellm_params={"litellm_call_id": "batch-retrieve-call-id", "metadata": {}},
)
result = await BaseLLMHTTPHandler().retrieve_batch(
batch_id="batch-1",
litellm_params={"api_key": provider_key},
provider_config=MistralBatchesConfig(),
headers={},
api_base=None,
api_key=provider_key,
logging_obj=logging_obj,
_is_async=True,
client=client,
model="mistral/mistral-ocr-latest",
)
assert result.id == "batch-1"
assert sent_requests[0].headers["Authorization"] == f"Bearer {provider_key}"
raw_request_body = logging_obj.model_call_details["raw_request_typed_dict"]["raw_request_body"]
assert provider_key not in json.dumps(raw_request_body)
@pytest.mark.asyncio
async def test_async_anthropic_messages_handler_carries_deployment_vertex_location_for_pricing(monkeypatch):
"""