From 91f23c6e161ab5bbe096e5903277da72ddb7c165 Mon Sep 17 00:00:00 2001 From: devteamaegis Date: Sun, 2 Aug 2026 16:51:19 -0400 Subject: [PATCH] fix(local-apigw): treat authorizer resource ARNs as literals, not regex _is_resource_authorized built a regular expression straight from the Lambda authorizer's Resource ARN, escaping nothing but the wildcards. Any regex metacharacter in the ARN was therefore interpreted as syntax. The common case is the HTTP API '$default' stage: '$' is an end-of-string anchor, so an Allow statement for 'arn:aws:execute-api:...:api/$default/*' never matched the method ARN and sam local start-api returned 403 for a request that succeeds when deployed - even when the authorizer echoed back the exact methodArn it was handed. A '[' or '(' in the path raised re.PatternError outright. Escape the ARN first, then translate the '*' and '?' wildcards. Signed-off-by: devteamaegis --- .../apigw/authorizers/lambda_authorizer.py | 6 +- .../local/apigw/test_lambda_authorizer.py | 60 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/samcli/local/apigw/authorizers/lambda_authorizer.py b/samcli/local/apigw/authorizers/lambda_authorizer.py index 21450ec2a4a..dc09522e46f 100644 --- a/samcli/local/apigw/authorizers/lambda_authorizer.py +++ b/samcli/local/apigw/authorizers/lambda_authorizer.py @@ -387,8 +387,10 @@ def _is_resource_authorized(self, response: dict, method_arn: str) -> bool: resource_list = resource if isinstance(resource, list) else [resource] for resource_arn in resource_list: - # form a regular expression from the possible wildcard resource ARN - regex_method_arn = resource_arn.replace("*", ".*").replace("?", ".") + # form a regular expression from the possible wildcard resource ARN; + # escape it first so that regex syntax in the ARN itself (such as the + # "$" in the HTTP API "$default" stage) is matched literally + regex_method_arn = re.escape(resource_arn).replace(r"\*", ".*").replace(r"\?", ".") regex_method_arn += "$" if re.match(regex_method_arn, method_arn): diff --git a/tests/unit/local/apigw/test_lambda_authorizer.py b/tests/unit/local/apigw/test_lambda_authorizer.py index d38934ea215..f5e9af08de6 100644 --- a/tests/unit/local/apigw/test_lambda_authorizer.py +++ b/tests/unit/local/apigw/test_lambda_authorizer.py @@ -455,6 +455,66 @@ def test_validate_is_resource_authorized( self.assertEqual(result, expected_result) + @parameterized.expand( + [ + ( # HTTP API "$default" stage, wildcard resource + "arn:aws:execute-api:us-east-1:123456789012:1234567890/$default/*", + "arn:aws:execute-api:us-east-1:123456789012:1234567890/$default/GET/hello", + True, + ), + ( # authorizer echoes the method ARN it was given back verbatim + "arn:aws:execute-api:us-east-1:123456789012:1234567890/$default/GET/hello", + "arn:aws:execute-api:us-east-1:123456789012:1234567890/$default/GET/hello", + True, + ), + ( # a "[" in the path must not be read as a character set + "arn:aws:execute-api:us-east-1:123456789012:1234567890/prod/GET/a[b", + "arn:aws:execute-api:us-east-1:123456789012:1234567890/prod/GET/a[b", + True, + ), + ( # a "(" in the path must not be read as a group + "arn:aws:execute-api:us-east-1:123456789012:1234567890/prod/GET/x(y", + "arn:aws:execute-api:us-east-1:123456789012:1234567890/prod/GET/x(y", + True, + ), + ( # "." is a literal, not a wildcard + "arn:aws:execute-api:us-east-1:123456789012:1234567890/prod/GET/a.c", + "arn:aws:execute-api:us-east-1:123456789012:1234567890/prod/GET/abc", + False, + ), + ( # a different stage is still denied + "arn:aws:execute-api:us-east-1:123456789012:1234567890/$default/*", + "arn:aws:execute-api:us-east-1:123456789012:1234567890/prod/GET/hello", + False, + ), + ] + ) + def test_is_resource_authorized_treats_arn_as_literal(self, resource_arn, method_arn, expected_result): + auth = LambdaAuthorizer( + "my auth", + Mock(), + Mock(), + [], + Mock(), + Mock(), + Mock(), + ) + response = { + "policyDocument": { + "Statement": [ + { + "Action": ["execute-api:Invoke"], + "Effect": "Allow", + "Resource": resource_arn, + } + ] + }, + } + + result = auth._is_resource_authorized(response, method_arn) + + self.assertEqual(result, expected_result) + class TestLambdaAuthorizerIamPolicyValidator(TestCase): @parameterized.expand(