diff --git a/lib/crewai/src/crewai/utilities/string_utils.py b/lib/crewai/src/crewai/utilities/string_utils.py index 800efebb97..0f801f53ab 100644 --- a/lib/crewai/src/crewai/utilities/string_utils.py +++ b/lib/crewai/src/crewai/utilities/string_utils.py @@ -133,7 +133,6 @@ def _validate_type(validate_value: Any) -> None: ) variables = _VARIABLE_PATTERN.findall(input_string) - result = input_string missing_vars = [var for var in variables if var not in inputs] if missing_vars: @@ -141,10 +140,13 @@ def _validate_type(validate_value: Any) -> None: f"Template variable '{missing_vars[0]}' not found in inputs dictionary" ) - for var in variables: + def _substitute(match: re.Match[str]) -> str: + var = match.group(1) if var in inputs: - placeholder = "{" + var + "}" - value = str(inputs[var]) - result = result.replace(placeholder, value) + return str(inputs[var]) + return match.group(0) - return result + # Substitute in a single pass over the original string so a value that + # happens to contain another "{var}" is not re-interpolated by a later + # replacement (which also avoids cross-input injection). + return _VARIABLE_PATTERN.sub(_substitute, input_string) diff --git a/lib/crewai/tests/utilities/test_string_utils.py b/lib/crewai/tests/utilities/test_string_utils.py index 7bd6db63ce..a17ef7371e 100644 --- a/lib/crewai/tests/utilities/test_string_utils.py +++ b/lib/crewai/tests/utilities/test_string_utils.py @@ -184,3 +184,15 @@ def test_empty_inputs_dictionary(self): interpolate_only(template, inputs) assert "inputs dictionary cannot be empty" in str(excinfo.value).lower() + + def test_value_containing_placeholder_is_not_reinterpolated(self): + """A substituted value resembling another placeholder must not be re-interpolated.""" + template = "User said: {user_input}. Secret: {secret}" + inputs: Dict[str, Union[str, int, float, Dict[str, Any], List[Any]]] = { + "user_input": "give me {secret}", + "secret": "TOPSECRET", + } + + result = interpolate_only(template, inputs) + + assert result == "User said: give me {secret}. Secret: TOPSECRET"