From f58f96f094b8ce68e053004995c7ea789f7ab516 Mon Sep 17 00:00:00 2001 From: yucheng-berriai Date: Wed, 17 Jun 2026 20:31:21 -0700 Subject: [PATCH] fix(bedrock): stop reflecting AWS profile name in logs and errors The verbose get_credentials debug log interpolated the real aws_profile_name, and a missing profile let the boto3 ProfileNotFound (which echoes the profile value) propagate to the caller and into the chained traceback. The log now records only whether a profile is set, and profile-lookup failures map to a generic AwsAuthError raised from None so the value is not reflected through logs, error messages, or the traceback chain. Regression tests cover the boolean-only log line and the generic, chain-suppressed error. --- litellm/llms/bedrock/base_aws_llm.py | 19 +++-- .../llms/bedrock/test_base_aws_llm.py | 83 ++++++++++++++++++- 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py index 2c9ea187912..0d06a9d510c 100644 --- a/litellm/llms/bedrock/base_aws_llm.py +++ b/litellm/llms/bedrock/base_aws_llm.py @@ -256,7 +256,8 @@ class BaseAWSLLM: "aws_session_token=[set=%s]\n" "aws_region_name=%s\n" "aws_session_name=%s\n" - "aws_profile_name=%s\n" + # log only whether aws_profile_name is set, not its value + "aws_profile_name=[set=%s]\n" "aws_role_name=%s\n" "aws_web_identity_token=[set=%s]\n" "aws_sts_endpoint=%s\n" @@ -266,7 +267,7 @@ class BaseAWSLLM: aws_session_token is not None, aws_region_name, aws_session_name, - aws_profile_name, + aws_profile_name is not None, aws_role_name, aws_web_identity_token is not None, aws_sts_endpoint, @@ -1234,9 +1235,17 @@ class BaseAWSLLM: import boto3 # uses auth values from AWS profile usually stored in ~/.aws/credentials - with tracer.trace("boto3.Session(profile_name=aws_profile_name)"): - client = boto3.Session(profile_name=aws_profile_name) - return client.get_credentials(), None + # map any failure to a generic error so the profile value is not + # reflected back in the exception message or the chained traceback + try: + with tracer.trace("boto3.Session(profile_name=aws_profile_name)"): + client = boto3.Session(profile_name=aws_profile_name) + return client.get_credentials(), None + except Exception: + raise AwsAuthError( + message="The specified AWS profile could not be found.", + status_code=400, + ) from None @tracer.wrap() def _auth_with_aws_session_token( diff --git a/tests/test_litellm/llms/bedrock/test_base_aws_llm.py b/tests/test_litellm/llms/bedrock/test_base_aws_llm.py index 3f91f6ac26e..bbf8d20f557 100644 --- a/tests/test_litellm/llms/bedrock/test_base_aws_llm.py +++ b/tests/test_litellm/llms/bedrock/test_base_aws_llm.py @@ -163,6 +163,77 @@ def test_aws_profile_path_not_cached_in_iam_cache(): assert mock_profile.call_count == 2 +def test_aws_profile_not_found_raises_generic_error(): + """boto3 ProfileNotFound carries the resolved profile name (which may + be an os.environ/ secret). _auth_with_aws_profile must map it to a generic error that + never reflects the user-supplied/resolved value into logs or tracebacks.""" + from botocore.exceptions import ProfileNotFound + + secret_profile_value = "super-secret-resolved-profile-name" + + fake_boto3 = MagicMock() + fake_boto3.Session.side_effect = ProfileNotFound(profile=secret_profile_value) + + base = BaseAWSLLM() + with patch.dict("sys.modules", {"boto3": fake_boto3}): + with pytest.raises(AwsAuthError) as exc_info: + base._auth_with_aws_profile(secret_profile_value) + + err = exc_info.value + assert secret_profile_value not in err.message + assert secret_profile_value not in str(err) + assert err.message == "The specified AWS profile could not be found." + + # the original ProfileNotFound (which embeds the profile value) must not + # be rendered as a chained cause/context, or a handler that logs the full + # traceback would re-leak it + assert err.__cause__ is None + assert err.__suppress_context__ is True + import traceback + + rendered = "".join(traceback.format_exception(type(err), err, err.__traceback__)) + assert secret_profile_value not in rendered + + +def test_get_credentials_does_not_log_aws_profile_value(): + """get_credentials logs aws_profile_name as a [set=True] boolean only. The + resolved profile value can itself be a secret (os.environ/ or an upper-case + env lookup), so it must never be reflected into the verbose debug log. This + fails if the format string is changed to interpolate the value (e.g. + 'aws_profile_name=%s').""" + import logging + + sentinel = "super-secret-profile-do-not-log" + + captured = [] + + class _ListHandler(logging.Handler): + def emit(self, record): + captured.append(record.getMessage()) + + handler = _ListHandler() + verbose_logger = logging.getLogger("LiteLLM") + previous_level = verbose_logger.level + verbose_logger.addHandler(handler) + verbose_logger.setLevel(logging.DEBUG) + + base = BaseAWSLLM() + try: + with patch.object( + base, + "_auth_with_aws_profile", + return_value=(Credentials("prof-ak", "prof-sk", None), None), + ): + base.get_credentials(aws_profile_name=sentinel) + finally: + verbose_logger.removeHandler(handler) + verbose_logger.setLevel(previous_level) + + combined = "\n".join(captured) + assert sentinel not in combined + assert "aws_profile_name=[set=True]" in combined + + def test_web_identity_path_not_cached_in_iam_cache(): base = BaseAWSLLM() with patch.object( @@ -880,7 +951,11 @@ def test_different_roles_without_session_names_should_not_share_cache(): }, ), ], - ids=["no_region_or_endpoint", "bedrock_region_ignored_for_sts", "explicit_sts_endpoint"], + ids=[ + "no_region_or_endpoint", + "bedrock_region_ignored_for_sts", + "explicit_sts_endpoint", + ], ) def test_eks_irsa_ambient_credentials_used(role_kwargs, expected_client_kwargs): """ @@ -1272,7 +1347,11 @@ def test_sts_endpoint_region_matches_bedrock_region_param(): }, ), ], - ids=["no_region_or_endpoint", "bedrock_region_ignored_for_sts", "explicit_sts_endpoint"], + ids=[ + "no_region_or_endpoint", + "bedrock_region_ignored_for_sts", + "explicit_sts_endpoint", + ], ) def test_explicit_credentials_used_when_provided(role_kwargs, expected_client_kwargs): """