Skip to content

Commit 698aefd

Browse files
committed
fix: surface user-facing errors for malformed Fn::If and malformed CodeUri
Three customer templates were crashing the SAM transform with unhandled exceptions that CloudFormation surfaced as the opaque message "Transform AWS::Serverless-2016-10-31 failed with: Internal transform failure.", leaving the customer with no indication of which resource or property was wrong. 1) AWS::Serverless::Function.Role supports Fn::If so a user can switch between an existing IAM role and a SAM-generated one. _make_lambda_role went straight from `is_intrinsic_if(role)` to a 3-tuple unpack: role_condition, role_if, role_else = role_resolved_value.get("Fn::If") A malformed 2- or 4-element list therefore raised ValueError: not enough values to unpack (expected 3, got 2) Wrap with validate_intrinsic_if_items (already used in resource_policies.py and preferences/deployment_preference_collection.py) and raise InvalidResourceException(self.logical_id, ...). 2) EventInvokeConfig.DestinationConfig.{OnSuccess,OnFailure}.Destination also supports Fn::If. _get_or_make_condition indexed dest_list[1] / dest_list[2] without arity validation, so a 2-element list raised IndexError: list index out of range Same guard using validate_intrinsic_if_items is applied here. 3) parse_s3_uri calls urllib.parse.urlparse, which has validated bracketed-host segments against the IPv6 grammar since the CVE-2024-11168 fix (Python 3.9.17+/3.10.12+/3.11.4+/3.12/3.13/3.14). Unresolved CDK tokens like "s3://[TOKEN.25]/key" now raise: ValueError: "TOKEN.25" does not appear to be an IPv4 or IPv6 address Catch ValueError and return None so construct_s3_location_object raises the existing friendly "CodeUri is not a valid S3 Uri..." InvalidResourceException with the logical id. Tests: - tests/translator/input/error_function_role_fn_if_malformed.{yaml,json} - tests/translator/input/error_function_codeuri_unresolved_token.{yaml,json} - tests/translator/input/error_function_event_destination_fn_if_malformed.{yaml,json} (auto-discovered by test_transform_invalid_document) - tests/model/s3_utils/test_uri_parser.py: parse_s3_uri returns None on malformed bracketed netloc; construct_s3_location_object raises InvalidResourceException with the resource logical id - tests/model/test_sam_resources.py: _make_lambda_role raises InvalidResourceException on too-few and too-many Fn::If items Full suite: 4503 passed, 0 failed.
1 parent b4aabe1 commit 698aefd

11 files changed

Lines changed: 246 additions & 2 deletions

samtranslator/model/s3_utils/uri_parser.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,17 @@ def parse_s3_uri(uri: Any) -> dict[str, Any] | None:
1414
if not isinstance(uri, str):
1515
return None
1616

17-
url = urlparse(uri)
17+
try:
18+
url = urlparse(uri)
19+
except ValueError:
20+
# Python's urllib validates bracketed host segments ("[...]") against
21+
# RFC 3986 IPv6/IPv4 grammars since the CVE-2024-11168 fix. Unresolved
22+
# CDK tokens (for example "s3://[TOKEN.25]/key") or other malformed
23+
# URIs therefore raise ValueError here. Treating the input as "not a
24+
# valid S3 URI" lets the caller raise the existing, user-friendly
25+
# InvalidResourceException with the resource logical id and property
26+
# name, instead of surfacing an opaque "Internal transform failure".
27+
return None
1828
query = parse_qs(url.query)
1929

2030
if url.scheme == "s3" and url.netloc and url.path:

samtranslator/model/sam_resources.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
make_conditional,
103103
make_not_conditional,
104104
ref,
105+
validate_intrinsic_if_items,
105106
)
106107
from samtranslator.model.lambda_ import (
107108
LAMBDA_TRACING_CONFIG_DISABLED,
@@ -452,7 +453,17 @@ def _make_lambda_role(
452453

453454
# We need to create and if else condition here
454455
role_resolved_value = intrinsics_resolver.resolve_parameter_refs(lambda_role)
455-
role_condition, role_if, role_else = role_resolved_value.get("Fn::If")
456+
if_items = role_resolved_value.get("Fn::If")
457+
try:
458+
validate_intrinsic_if_items(if_items)
459+
except ValueError as e:
460+
# Surface a user-facing error instead of letting a ValueError propagate
461+
# all the way out of the transform macro as "Internal transform failure".
462+
raise InvalidResourceException(
463+
self.logical_id,
464+
f"Malformed 'Role' property: {e!s}.",
465+
) from e
466+
role_condition, role_if, role_else = if_items
456467

457468
if is_intrinsic_no_value(role_if) and is_intrinsic_no_value(role_else):
458469
lambda_role_value = execution_role_arn
@@ -627,6 +638,15 @@ def _get_or_make_condition(self, destination: Any, logical_id: str, conditions:
627638
return None, None
628639
if is_intrinsic_if(destination):
629640
dest_list = destination.get("Fn::If")
641+
try:
642+
validate_intrinsic_if_items(dest_list)
643+
except ValueError as e:
644+
# Surface a user-facing error instead of letting an IndexError
645+
# propagate out of the transform macro as "Internal transform failure".
646+
raise InvalidResourceException(
647+
logical_id,
648+
f"Malformed 'Destination' property: {e!s}.",
649+
) from e
630650
if is_intrinsic_no_value(dest_list[1]) and is_intrinsic_no_value(dest_list[2]):
631651
return None, None
632652
if is_intrinsic_no_value(dest_list[1]):

tests/model/s3_utils/__init__.py

Whitespace-only changes.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Unit tests for :mod:`samtranslator.model.s3_utils.uri_parser`."""
2+
3+
from unittest import TestCase
4+
5+
from samtranslator.model.exceptions import InvalidResourceException
6+
from samtranslator.model.s3_utils.uri_parser import construct_s3_location_object, parse_s3_uri
7+
8+
9+
class TestParseS3Uri(TestCase):
10+
def test_valid_s3_uri(self):
11+
self.assertEqual(
12+
parse_s3_uri("s3://bucket/key"),
13+
{"Bucket": "bucket", "Key": "key"},
14+
)
15+
16+
def test_valid_s3_uri_with_version(self):
17+
self.assertEqual(
18+
parse_s3_uri("s3://bucket/key?versionId=abcdef"),
19+
{"Bucket": "bucket", "Key": "key", "Version": "abcdef"},
20+
)
21+
22+
def test_non_s3_scheme_returns_none(self):
23+
self.assertIsNone(parse_s3_uri("https://example.com/key"))
24+
25+
def test_non_string_returns_none(self):
26+
self.assertIsNone(parse_s3_uri({"Bucket": "b", "Key": "k"}))
27+
self.assertIsNone(parse_s3_uri(None))
28+
29+
def test_unresolved_cdk_token_returns_none(self):
30+
"""Bracketed host segments that are not valid IPv4/IPv6 raise ValueError
31+
from urllib (see CVE-2024-11168); parse_s3_uri should treat the input as
32+
"not a valid S3 URI" and return None so callers can raise a friendly
33+
InvalidResourceException instead of crashing the transform.
34+
"""
35+
self.assertIsNone(parse_s3_uri("s3://[TOKEN.25]/my/key"))
36+
self.assertIsNone(parse_s3_uri("https://[TOKEN.25]/path"))
37+
self.assertIsNone(parse_s3_uri("s3://bucket-[TOKEN.25]/key"))
38+
39+
40+
class TestConstructS3LocationObjectWithMalformedUri(TestCase):
41+
"""Verify that the top-level helper raises InvalidResourceException with the
42+
logical id instead of letting the underlying urllib ValueError propagate.
43+
"""
44+
45+
def test_unresolved_cdk_token_raises_invalid_resource_exception(self):
46+
with self.assertRaises(InvalidResourceException) as ctx:
47+
construct_s3_location_object("s3://[TOKEN.25]/my/key", "MyFunction", "CodeUri")
48+
self.assertIn("MyFunction", str(ctx.exception))
49+
self.assertIn("'CodeUri' is not a valid S3 Uri", str(ctx.exception))

tests/model/test_sam_resources.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from unittest.mock import patch
33

44
import pytest
5+
from parameterized import parameterized
56
from samtranslator.intrinsics.resolver import IntrinsicsResolver
67
from samtranslator.model import InvalidResourceException, ResourceResolver
78
from samtranslator.model.apigateway import ApiGatewayDeployment, ApiGatewayRestApi, ApiGatewayStage
@@ -847,6 +848,51 @@ def test_role_get_att_no_execution_role(self):
847848

848849
self.assertEqual(lambda_function.Role, role_get_att)
849850

851+
@parameterized.expand(
852+
[
853+
# 2-arg Fn::If (missing false value)
854+
({"Fn::If": ["Condition", "arn:aws:iam::123456789012:role/existing-role"]},),
855+
# 4-arg Fn::If (extra value)
856+
({"Fn::If": ["Condition", "role_a", "role_b", "role_c"]},),
857+
]
858+
)
859+
def test_role_fn_if_invalid_arity_raises_invalid_resource_exception(self, malformed_role):
860+
"""Fn::If must have exactly 3 items: [Condition, TrueValue, FalseValue].
861+
Any other arity used to crash SAM-T with a raw ValueError/IndexError,
862+
which CloudFormation surfaced as "Internal transform failure".
863+
It must now raise a user-facing InvalidResourceException instead.
864+
"""
865+
self.function.Role = malformed_role
866+
867+
with pytest.raises(InvalidResourceException) as excinfo:
868+
self.function.to_cloudformation(**self.kwargs)
869+
870+
msg = str(excinfo.value)
871+
self.assertIn("foo", msg) # logical id
872+
self.assertIn("Role", msg)
873+
self.assertIn("Fn::If", msg)
874+
875+
@parameterized.expand(
876+
[
877+
# 2-arg Fn::If (missing false value)
878+
({"Fn::If": ["SomeCondition", "queue-arn"]},),
879+
# 4-arg Fn::If (extra value)
880+
({"Fn::If": ["SomeCondition", "a", "b", "c"]},),
881+
]
882+
)
883+
def test_destination_fn_if_invalid_arity_raises_invalid_resource_exception(self, malformed):
884+
"""EventInvokeConfig.DestinationConfig.{OnSuccess,OnFailure}.Destination
885+
also supports Fn::If. Any list arity other than 3 previously crashed the
886+
transform with IndexError in _get_or_make_condition.
887+
"""
888+
with pytest.raises(InvalidResourceException) as excinfo:
889+
self.function._get_or_make_condition(malformed, "DestLogicalId", {})
890+
891+
msg = str(excinfo.value)
892+
self.assertIn("DestLogicalId", msg)
893+
self.assertIn("Destination", msg)
894+
self.assertIn("Fn::If", msg)
895+
850896

851897
class TestSamCapacityProvider(TestCase):
852898
"""Tests for SamCapacityProvider"""
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Resources:
2+
MyFunction:
3+
Type: AWS::Serverless::Function
4+
Properties:
5+
Runtime: python3.12
6+
Handler: index.handler
7+
# CDK sometimes synthesizes unresolved Token placeholders like
8+
# "${Token[Bucket.Name.25]}" / "[TOKEN.25]" into string properties.
9+
# Python's urllib validates bracketed-host segments against the IPv6
10+
# grammar since CVE-2024-11168, so urlparse("s3://[TOKEN.25]/...")
11+
# raises `ValueError: 'TOKEN.25' does not appear to be an IPv4 or IPv6
12+
# address`. Previously that propagated out of the transform macro as
13+
# the opaque "Internal transform failure" message.
14+
CodeUri: s3://[TOKEN.25]/my/key
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Parameters:
2+
IsProd:
3+
Type: String
4+
Default: 'true'
5+
6+
Conditions:
7+
IsProdCondition: !Equals [!Ref IsProd, 'true']
8+
9+
Resources:
10+
MyQueue:
11+
Type: AWS::SQS::Queue
12+
13+
MyFunction:
14+
Type: AWS::Serverless::Function
15+
Properties:
16+
Runtime: python3.12
17+
Handler: index.handler
18+
InlineCode: |
19+
def handler(event, context):
20+
return 'hi'
21+
EventInvokeConfig:
22+
MaximumRetryAttempts: 2
23+
DestinationConfig:
24+
OnSuccess:
25+
Type: SQS
26+
# Malformed Fn::If: only 2 elements instead of [Condition, TrueValue, FalseValue].
27+
# Previously this crashed the transform with an IndexError in
28+
# _get_or_make_condition, which CloudFormation surfaced as
29+
# "Internal transform failure". It must now raise a user-facing
30+
# InvalidResourceException naming the offending resource.
31+
Destination: !If [IsProdCondition, !GetAtt MyQueue.Arn]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Parameters:
2+
IsProd:
3+
Type: String
4+
Default: 'true'
5+
6+
Conditions:
7+
IsProdCondition: !Equals [!Ref IsProd, 'true']
8+
9+
Resources:
10+
ExistingRole:
11+
Type: AWS::IAM::Role
12+
Properties:
13+
AssumeRolePolicyDocument:
14+
Version: '2012-10-17'
15+
Statement:
16+
- Effect: Allow
17+
Principal: {Service: lambda.amazonaws.com}
18+
Action: sts:AssumeRole
19+
20+
MyFunction:
21+
Type: AWS::Serverless::Function
22+
Properties:
23+
Runtime: python3.12
24+
Handler: index.handler
25+
InlineCode: |
26+
def handler(event, context):
27+
return 'hi'
28+
# Malformed Fn::If: only 2 elements instead of [Condition, TrueValue, FalseValue].
29+
# Previously this crashed the transform with a Python
30+
# `ValueError: not enough values to unpack (expected 3, got 2)`,
31+
# which CloudFormation surfaced as "Internal transform failure".
32+
Role: !If [IsProdCondition, !GetAtt ExistingRole.Arn]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"_autoGeneratedBreakdownErrorMessage": [
3+
"Invalid Serverless Application Specification document. ",
4+
"Number of errors found: 1. ",
5+
"Resource with id [MyFunction] is invalid. ",
6+
"'CodeUri' is not a valid S3 Uri of the form 's3://bucket/key' with optional versionId query parameter."
7+
],
8+
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyFunction] is invalid. 'CodeUri' is not a valid S3 Uri of the form 's3://bucket/key' with optional versionId query parameter.",
9+
"errors": [
10+
{
11+
"errorMessage": "Resource with id [MyFunction] is invalid. 'CodeUri' is not a valid S3 Uri of the form 's3://bucket/key' with optional versionId query parameter."
12+
}
13+
]
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"_autoGeneratedBreakdownErrorMessage": [
3+
"Invalid Serverless Application Specification document. ",
4+
"Number of errors found: 1. ",
5+
"Resource with id [MyFunctionEventInvokeConfig] is invalid. ",
6+
"Malformed 'Destination' property: Fn::If requires 3 arguments."
7+
],
8+
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyFunctionEventInvokeConfig] is invalid. Malformed 'Destination' property: Fn::If requires 3 arguments.",
9+
"errors": [
10+
{
11+
"errorMessage": "Resource with id [MyFunctionEventInvokeConfig] is invalid. Malformed 'Destination' property: Fn::If requires 3 arguments."
12+
}
13+
]
14+
}

0 commit comments

Comments
 (0)