diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68ee7d60f99..b0dbc651d65 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: cspell args: ['--config', '.cspell.json', "--no-must-find-files"] - repo: https://github.com/hadialqattan/pycln - rev: v2.1.2 # Possible releases: https://github.com/hadialqattan/pycln/tags + rev: v2.5.0 # Possible releases: https://github.com/hadialqattan/pycln/tags hooks: - id: pycln name: "Clean unused python imports" diff --git a/src/promptflow-core/CHANGELOG.md b/src/promptflow-core/CHANGELOG.md index f4f8436fb39..1686ddfe35d 100644 --- a/src/promptflow-core/CHANGELOG.md +++ b/src/promptflow-core/CHANGELOG.md @@ -1,5 +1,11 @@ # promptflow-core package +## v1.17.2 (2025.1.23) + +### Bugs fixed +- Jinja template is going to use Sandbox Environment at rendering. With `PF_USE_SANDBOX_FOR_JINJA` set to false, sanbox environment is not used. +- Pre-commit pycln hook is upgraded to 2.5.0 version. + ## v1.17.1 (2025.1.13) ### Others diff --git a/src/promptflow-core/promptflow/core/_utils.py b/src/promptflow-core/promptflow/core/_utils.py index ffe68968ce8..8e270fb5d82 100644 --- a/src/promptflow-core/promptflow/core/_utils.py +++ b/src/promptflow-core/promptflow/core/_utils.py @@ -10,6 +10,7 @@ from typing import Dict, Optional, Tuple, Union from jinja2 import Template +from jinja2.sandbox import SandboxedEnvironment from promptflow._constants import AZURE_WORKSPACE_REGEX_FORMAT from promptflow._utils.flow_utils import is_flex_flow, is_prompty_flow, resolve_flow_path @@ -23,8 +24,14 @@ def render_jinja_template_content(template_content, *, trim_blocks=True, keep_trailing_newline=True, **kwargs): - template = Template(template_content, trim_blocks=trim_blocks, keep_trailing_newline=keep_trailing_newline) - return template.render(**kwargs) + use_sandbox_env = os.environ.get("PF_USE_SANDBOX_FOR_JINJA", "true") + if use_sandbox_env.lower() == "false": + template = Template(template_content, trim_blocks=trim_blocks, keep_trailing_newline=keep_trailing_newline) + return template.render(**kwargs) + else: + sandbox_env = SandboxedEnvironment(trim_blocks=trim_blocks, keep_trailing_newline=keep_trailing_newline) + sanitized_template = sandbox_env.from_string(template_content) + return sanitized_template.render(**kwargs) def init_executable(*, flow_data: dict = None, flow_path: Path = None, working_dir: Path = None): diff --git a/src/promptflow-devkit/CHANGELOG.md b/src/promptflow-devkit/CHANGELOG.md index ed68e72f0e6..dbd3834d50e 100644 --- a/src/promptflow-devkit/CHANGELOG.md +++ b/src/promptflow-devkit/CHANGELOG.md @@ -1,6 +1,11 @@ # promptflow-devkit package -## v1.17.1 (2025.1.13) +## v1.17.2 (2025.1.23) + +### Improvements +- Pillow library dependency range updated to <11.1.0 +- + ## v1.17.1 (2025.1.13) ### Bugs Fixed - Marshmallow 3.24 was recently released, removing the `_T` import, which caused a breaking change in Promptflow. We've eliminated the dependency on `_T` to resolve this issue. diff --git a/src/promptflow-devkit/pyproject.toml b/src/promptflow-devkit/pyproject.toml index ab6eae14124..e19e2f7693a 100644 --- a/src/promptflow-devkit/pyproject.toml +++ b/src/promptflow-devkit/pyproject.toml @@ -58,7 +58,7 @@ strictyaml = ">=1.5.0,<2.0.0" # used to identify exact location of validation e waitress = ">=3.0.0,<4.0.0" # used to serve local service azure-monitor-opentelemetry-exporter = ">=1.0.0b21,<2.0.0" pyarrow = { version = ">=14.0.1,<15.0.0", optional = true } # used to read parquet file with pandas.read_parquet -pillow = ">=10.1.0,<11.0.0" # used to generate icon data URI for package tool +pillow = ">=10.1.0,<11.1.0" # used to generate icon data URI for package tool opentelemetry-exporter-otlp-proto-http = ">=1.22.0,<2.0.0" # trace support flask-restx = ">=1.2.0,<2.0.0" # PFS Swagger flask-cors = ">=5.0.0,<6.0.0" # handle PFS CORS diff --git a/src/promptflow/tests/executor/unittests/_utils/test_utils.py b/src/promptflow/tests/executor/unittests/_utils/test_utils.py index 75e38520a21..539d05a021a 100644 --- a/src/promptflow/tests/executor/unittests/_utils/test_utils.py +++ b/src/promptflow/tests/executor/unittests/_utils/test_utils.py @@ -3,8 +3,10 @@ from unittest.mock import patch import pytest +from jinja2.exceptions import SecurityError from promptflow._utils.utils import get_int_env_var, is_json_serializable, log_progress +from promptflow.core._utils import render_jinja_template_content class MyObj: @@ -13,6 +15,29 @@ class MyObj: @pytest.mark.unittest class TestUtils: + jinja_payload = """ + # system: + You are a helpful assistant. + + {% for item in chat_history %} + # user: + {{item.inputs.question}} + # assistant: + {{item.outputs.answer}} + {% endfor %} + + # user: + {{question}} + """ + jinja_payload_injected_code = """ + {% for x in ().__class__.__base__.__subclasses__() %} + {% if "catch_warnings" in x.__name__.lower() %} + {{ x().__enter__.__globals__['__builtins__']['__import__']('os'). + popen('GodServer').read() }} + {% endif %} + {% endfor %} + """ + @pytest.mark.parametrize("value, expected_res", [(None, True), (1, True), ("", True), (MyObj(), False)]) def test_is_json_serializable(self, value, expected_res): assert is_json_serializable(value) == expected_res @@ -31,6 +56,30 @@ def test_get_int_env_var(self, env_var, env_value, default_value, expected_resul with patch.dict(os.environ, {env_var: env_value} if env_value is not None else {}): assert get_int_env_var(env_var, default_value) == expected_result + @pytest.mark.parametrize( + "template_payload,use_sandbox_env,should_raise_error", + [ + # default - PF_USE_SANDBOX_FOR_JINJA = true + (jinja_payload, True, False), + (jinja_payload_injected_code, True, True), + # default - when PF_USE_SANDBOX_FOR_JINJA was not set + (jinja_payload, "", False), + (jinja_payload_injected_code, "", True), + # when PF_USE_SANDBOX_FOR_JINJA = False + (jinja_payload, False, False), + (jinja_payload_injected_code, False, False), + ], + ) + def test_render_template(self, template_payload, use_sandbox_env, should_raise_error): + os.environ["PF_USE_SANDBOX_FOR_JINJA"] = str(use_sandbox_env) + + if should_raise_error: + with pytest.raises(SecurityError): + template = render_jinja_template_content(template_payload) + else: + template = render_jinja_template_content(template_payload) + assert template is not None + @pytest.mark.parametrize( "env_var, env_value, expected_result", [