From 6452a48e040f8c8d8fd525e328d88a0c86823e3a Mon Sep 17 00:00:00 2001 From: Osamaali313 <86572800+Osamaali313@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:45:58 +0300 Subject: [PATCH] fix: interpolate_only should substitute in a single pass interpolate_only substituted each placeholder with result.replace() against the running result, so a value substituted for an earlier {var} that itself contained text like {another_var} was re-interpolated by a later iteration. This produced incorrect output and let one input inject another input's value (e.g. a secret). The docstring and existing tests describe single-pass substitution. Use re.sub with a replacer over the original string so each placeholder is replaced exactly once and substituted text is never re-scanned. Existing behavior (multiple occurrences, untouched JSON braces, preserved {123}/{!var}, missing-variable KeyError) is unchanged. Adds a regression test. --- lib/crewai/src/crewai/utilities/string_utils.py | 14 ++++++++------ lib/crewai/tests/utilities/test_string_utils.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) 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"