Merge pull request #39044 from BerriAI/litellm_fix_bedrock_guardrail_token_log

fix(bedrock): mask signed request headers in guardrail debug log
This commit is contained in:
Mateo Wang 2026-08-31 23:30:14 -07:00 • committed by GitHub
commit 9f6024bf97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 57 additions and 2 deletions

View file

@ -32,6 +32,9 @@ from litellm.constants import BEDROCK_APPLY_GUARDRAIL_CHUNK_BUDGET_CHARS
from litellm.exceptions import ModifyResponseException
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.litellm_core_utils.core_helpers import redact_nested_match_and_regex_keys
from litellm.litellm_core_utils.litellm_logging import (
_get_masked_values, # pyright: ignore[reportPrivateUsage] # the shared header-masking helper has no public name
)
from litellm.litellm_core_utils.llm_cost_calc.guardrail_cost import bedrock_guardrail_cost
from litellm.llms.anthropic.chat.guardrail_translation.handler import AnthropicMessagesHandler
from litellm.llms.base_llm.guardrail_translation.utils import (
@ -1170,11 +1173,12 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
aws_region_name=aws_region_name,
api_key=api_key,
)
headers_dict: Final = dict(prepared_request.headers) # mutable-ok: the masking helper requires a dict
verbose_proxy_logger.debug(
"Bedrock AI request body: %s, url %s, headers: %s",
bedrock_request_data,
prepared_request.url,
prepared_request.headers,
_get_masked_values(headers_dict),
)
httpx_response: Final = await self._sign_and_post(

View file

@ -5590,3 +5590,52 @@ async def test_streaming_end_of_stream_block_emits_error_frame_instead_of_trunca
assert payload["error"]["message"] == "Violated guardrail policy"
assert payload["error"]["code"] == "400"
assert payload["error"]["provider_specific_fields"]["guardrailIdentifier"] == "test-guardrail"
@pytest.mark.asyncio
async def test_apply_guardrail_debug_log_masks_signed_request_headers():
import logging
from litellm._logging import verbose_proxy_logger
session_token = "FakeSessionTokenValueThatMustNeverAppearInLogs1234567890"
guardrail = BedrockGuardrail(
guardrailIdentifier="test-guardrail",
guardrailVersion="DRAFT",
aws_access_key_id="ASIAFAKEACCESSKEYID1",
aws_secret_access_key="fakeSecretAccessKeyForSigning",
aws_session_token=session_token,
aws_region_name="us-east-1",
)
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"action": "NONE", "outputs": []}
captured_records: list[logging.LogRecord] = []
class _RecordingHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
captured_records.append(record)
handler = _RecordingHandler(level=logging.DEBUG)
previous_level = verbose_proxy_logger.level
verbose_proxy_logger.addHandler(handler)
verbose_proxy_logger.setLevel(logging.DEBUG)
try:
with patch.object(guardrail.async_handler, "post", new_callable=AsyncMock) as mock_post:
mock_post.return_value = mock_response
await guardrail.make_bedrock_api_request(
source="INPUT",
messages=[{"role": "user", "content": "hello"}],
request_data={},
)
finally:
verbose_proxy_logger.removeHandler(handler)
verbose_proxy_logger.setLevel(previous_level)
rendered_messages = [record.getMessage() for record in captured_records]
header_lines = [message for message in rendered_messages if "headers:" in message]
assert header_lines, "expected the signed-request debug line to be logged"
assert any("X-Amz-Security-Token" in message for message in header_lines)
assert all(session_token not in message for message in rendered_messages)

View file

@ -917,7 +917,9 @@ async def test_bedrock_guardrail_make_api_request_passes_api_key():
"Content-Type": "application/json",
"Authorization": "Bearer test-api-key-789",
}
mock_request_instance.prepare.return_value = Mock()
mock_request_instance.prepare.return_value = Mock(
headers=mock_request_instance.headers
)
mock_aws_request.return_value = mock_request_instance
await guardrail_hook.make_bedrock_api_request(