From 8ec65bb5cdad5414aa2174e2fb2ac3178809533e Mon Sep 17 00:00:00 2001 From: Kevin DeJong Date: Mon, 15 Jul 2024 11:51:21 -0700 Subject: [PATCH] Fix an issue with endless loops in Fn::Sub (#3503) --- src/cfnlint/rules/functions/Sub.py | 37 ++++++++++----------------- test/unit/rules/functions/test_sub.py | 25 +++++++++++++++--- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/cfnlint/rules/functions/Sub.py b/src/cfnlint/rules/functions/Sub.py index 9e4c5b708a..162cb9bdf3 100644 --- a/src/cfnlint/rules/functions/Sub.py +++ b/src/cfnlint/rules/functions/Sub.py @@ -81,7 +81,7 @@ def _clean_error( return err def _validate_string( - self, validator: Validator, key: str, instance: Any + self, validator: Validator, key: str, instance: Any, sub_values: dict[str, Any] ) -> ValidationResult: params = re.findall(REGEX_SUB_PARAMETERS, instance) validator = validator.evolve( @@ -102,47 +102,38 @@ def _validate_string( for err in validator.descend( instance={"Fn::GetAtt": param}, schema={"type": ["string"]}, - path=key, ): yield self._clean_error(err, {"Fn::GetAtt": param}, param) else: - for err in validator.descend( - instance={"Ref": param}, - schema={"type": ["string"]}, - path=key, - ): - yield self._clean_error(err, {"Ref": param}, param) + if param not in sub_values: + for err in validator.descend( + instance={"Ref": param}, + schema={"type": ["string"]}, + ): + yield self._clean_error(err, {"Ref": param}, param) def fn_sub( self, validator: Validator, s: Any, instance: Any, schema: Any ) -> ValidationResult: validator = validator.evolve( - context=validator.context.evolve(strict_types=True) + context=validator.context.evolve(strict_types=True), ) - yield from super().validate(validator, s, instance, schema) + errs = list(super().validate(validator, s, instance, schema)) + if errs: + yield from iter(errs) + return key, value = self.key_value(instance) + sub_values = {} if validator.is_type(value, "array"): - if len(value) != 2: - return - if not validator.is_type(value[1], "object"): - return - sub_values = value[1].copy() for sub_k, sub_v in value[1].items(): fn_k, fn_v = is_function(sub_v) if fn_k == "Ref" and fn_v == sub_k: del sub_values[sub_k] - validator_string = validator.evolve( - context=validator.context.evolve(ref_values=sub_values) - ) value = value[0] - elif validator.is_type(value, "string"): - validator_string = validator - else: - return - errs = list(self._validate_string(validator_string, key, value)) + errs = list(self._validate_string(validator, key, value, sub_values)) if errs: yield from iter(errs) return diff --git a/test/unit/rules/functions/test_sub.py b/test/unit/rules/functions/test_sub.py index 2499cbbab1..70512b925d 100644 --- a/test/unit/rules/functions/test_sub.py +++ b/test/unit/rules/functions/test_sub.py @@ -10,6 +10,7 @@ from cfnlint.context import create_context_for_template from cfnlint.jsonschema import CfnTemplateValidator, ValidationError from cfnlint.rules.functions.GetAtt import GetAtt +from cfnlint.rules.functions.GetAz import GetAz from cfnlint.rules.functions.Ref import Ref from cfnlint.rules.functions.Sub import Sub from cfnlint.template import Template @@ -199,10 +200,26 @@ def context(cfn): ), validator="fn_sub", ), + ], + ), + ( + "Invalid Fn::Sub with a bad object type from fn", + { + "Fn::Sub": [ + "${foo}", + { + "foo": {"Fn::GetAZs": ""}, + }, + ] + }, + {"type": "string"}, + [ ValidationError( - ("'foo' is not of type 'string' " "when 'Ref' is resolved"), - path=deque(["Fn::Sub"]), - schema_path=deque([]), + "{'Fn::GetAZs': ''} is not of type 'string'", + path=deque(["Fn::Sub", 1, "foo"]), + schema_path=deque( + ["fn_items", "patternProperties", "[a-zA-Z0-9]+", "fn_getazs"] + ), validator="fn_sub", ), ], @@ -305,7 +322,9 @@ def test_validate(name, instance, schema, expected, rule, context, cfn): validators={ "ref": Ref().ref, "fn_getatt": GetAtt().fn_getatt, + "fn_getazs": GetAz().fn_getazs, } )(context=context, cfn=cfn) errs = list(rule.fn_sub(validator, schema, instance, {})) + assert errs == expected, f"Test {name!r} got {errs!r}"