From fa36cca20096f1515fd95db2fe15c5b794a118bf Mon Sep 17 00:00:00 2001 From: ShussMiao <168898188+ShussMiao@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:41:47 +0800 Subject: [PATCH] fix(parameters): stop SecretsProvider.get_multiple mutating the caller's Filters list `_get_multiple` read `Filters` straight out of `sdk_options` and appended the `name` filter onto that same list object. When a caller passes a shared or module-level `Filters` value, every call leaves one extra `name` filter behind, so the next request goes out with a stale filter and can return secrets that were never asked for. Copy the list before appending so the caller's value is left untouched. Signed-off-by: ShussMiao <168898188+ShussMiao@users.noreply.github.com> --- .../utilities/parameters/secrets.py | 5 ++- .../_boto3/test_utilities_parameters.py | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index ed4da824dd1..aa5653432b3 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -152,8 +152,9 @@ def _get_multiple(self, names: list[str], **sdk_options) -> dict[str, Any]: # t When the parameter provider fails to retrieve secrets """ - # Merge filters: combine names with any additional filters from sdk_options - filters = sdk_options.get("Filters", []) + # Merge filters: combine names with any additional filters from sdk_options. + # Copy the caller's list so a shared or module-level Filters value isn't mutated. + filters = list(sdk_options.get("Filters", [])) name_filter = {"Key": "name", "Values": names} # Add name filter to existing filters diff --git a/tests/functional/parameters/_boto3/test_utilities_parameters.py b/tests/functional/parameters/_boto3/test_utilities_parameters.py index 8442e4c9113..04ea87fc046 100644 --- a/tests/functional/parameters/_boto3/test_utilities_parameters.py +++ b/tests/functional/parameters/_boto3/test_utilities_parameters.py @@ -3461,6 +3461,51 @@ def test_secrets_provider_get_multiple_basic(config): stubber.deactivate() +def test_secrets_provider_get_multiple_does_not_mutate_caller_filters(config): + """ + Test SecretsProvider.get_multiple() leaves the caller's Filters list untouched + """ + # GIVEN a Filters list owned by the caller + provider = parameters.SecretsProvider(boto_config=config) + caller_filters = [{"Key": "primary-region", "Values": ["us-east-1"]}] + original_filters = [{"Key": "primary-region", "Values": ["us-east-1"]}] + + # Stub the boto3 client to expect the caller filter plus exactly one name filter per call + stubber = stub.Stubber(provider.client) + for name in ["db-password", "api-key"]: + response = { + "SecretValues": [ + { + "ARN": f"arn:aws:secretsmanager:us-east-1:132456789012:secret/{name}", + "Name": name, + "VersionId": "7a9155b8-2dc9-466e-b4f6-5bc46516c84d", + "SecretString": "super-secret-password", + "CreatedDate": datetime(2015, 1, 1), + }, + ], + "Errors": [], + } + expected_params = { + "Filters": [ + {"Key": "primary-region", "Values": ["us-east-1"]}, + {"Key": "name", "Values": [name]}, + ], + } + stubber.add_response("batch_get_secret_value", response, expected_params) + stubber.activate() + + try: + # WHEN the same caller-owned list is reused across two calls + provider.get_multiple(["db-password"], Filters=caller_filters) + provider.get_multiple(["api-key"], Filters=caller_filters) + + # THEN the caller's list must not have grown a stale name filter + assert caller_filters == original_filters + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + def test_secrets_provider_get_multiple_with_binary(config): """ Test SecretsProvider.get_multiple() with binary secrets