From 0e1aedb9f4676e855939a9624bf8e2a9e442006c Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Wed, 1 Nov 2023 17:54:01 -0400 Subject: [PATCH] Use jinja2 sandboxing by default (#12733) * This is an opt-in feature, so users should be aware of risks if using jinja2. * Regardless we'll add sandboxing by default to jinja2 templates -- this sandboxing is a best effort basis. * Best strategy is still to make sure that jinja2 templates are only loaded from trusted sources. --- libs/langchain/langchain/prompts/base.py | 24 ++++++++++++++---- libs/langchain/langchain/prompts/prompt.py | 25 ++++++++++++++++--- .../tests/unit_tests/prompts/test_prompt.py | 11 ++++++++ 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/libs/langchain/langchain/prompts/base.py b/libs/langchain/langchain/prompts/base.py index 04b522f733b..f2744c43a14 100644 --- a/libs/langchain/langchain/prompts/base.py +++ b/libs/langchain/langchain/prompts/base.py @@ -15,19 +15,33 @@ from langchain.utils.formatting import formatter def jinja2_formatter(template: str, **kwargs: Any) -> str: """Format a template using jinja2. - *Security warning*: jinja2 templates are not sandboxed and may lead - to arbitrary Python code execution. Do not expand jinja2 templates - using unverified or user-controlled inputs! + *Security warning*: As of LangChain 0.0.329, this method uses Jinja2's + SandboxedEnvironment by default. However, this sand-boxing should + be treated as a best-effort approach rather than a guarantee of security. + Do not accept jinja2 templates from untrusted sources as they may lead + to arbitrary Python code execution. + + https://jinja.palletsprojects.com/en/3.1.x/sandbox/ """ try: - from jinja2 import Template + from jinja2.sandbox import SandboxedEnvironment except ImportError: raise ImportError( "jinja2 not installed, which is needed to use the jinja2_formatter. " "Please install it with `pip install jinja2`." + "Please be cautious when using jinja2 templates. " + "Do not expand jinja2 templates using unverified or user-controlled " + "inputs as that can result in arbitrary Python code execution." ) - return Template(template).render(**kwargs) + # This uses a sandboxed environment to prevent arbitrary code execution. + # Jinja2 uses an opt-out rather than opt-in approach for sand-boxing. + # Please treat this sand-boxing as a best-effort approach rather than + # a guarantee of security. + # We recommend to never use jinja2 templates with untrusted inputs. + # https://jinja.palletsprojects.com/en/3.1.x/sandbox/ + # approach not a guarantee of security. + return SandboxedEnvironment().from_string(template).render(**kwargs) def validate_jinja2(template: str, input_variables: List[str]) -> None: diff --git a/libs/langchain/langchain/prompts/prompt.py b/libs/langchain/langchain/prompts/prompt.py index 27ee992f8e2..e935d15aae3 100644 --- a/libs/langchain/langchain/prompts/prompt.py +++ b/libs/langchain/langchain/prompts/prompt.py @@ -22,9 +22,16 @@ class PromptTemplate(StringPromptTemplate): The template can be formatted using either f-strings (default) or jinja2 syntax. *Security warning*: Prefer using `template_format="f-string"` instead of - `template_format="jinja2"`, since jinja2 templates are not sandboxed and may - lead to arbitrary Python code execution. Do not construct a jinja2 `PromptTemplate` - from unverified or user-controlled inputs! + `template_format="jinja2"`, or make sure to NEVER accept jinja2 templates + from untrusted sources as they may lead to arbitrary Python code execution. + + As of LangChain 0.0.329, Jinja2 templates will be rendered using + Jinja2's SandboxedEnvironment by default. This sand-boxing should + be treated as a best-effort approach rather than a guarantee of security, + as it is an opt-out rather than opt-in approach. + + Despite the sand-boxing, we recommend to never use jinja2 templates + from untrusted sources. Example: @@ -196,6 +203,18 @@ class PromptTemplate(StringPromptTemplate): ) -> PromptTemplate: """Load a prompt template from a template. + *Security warning*: Prefer using `template_format="f-string"` instead of + `template_format="jinja2"`, or make sure to NEVER accept jinja2 templates + from untrusted sources as they may lead to arbitrary Python code execution. + + As of LangChain 0.0.329, Jinja2 templates will be rendered using + Jinja2's SandboxedEnvironment by default. This sand-boxing should + be treated as a best-effort approach rather than a guarantee of security, + as it is an opt-out rather than opt-in approach. + + Despite the sand-boxing, we recommend to never use jinja2 templates + from untrusted sources. + Args: template: The template to load. template_format: The format of the template. Use `jinja2` for jinja2, diff --git a/libs/langchain/tests/unit_tests/prompts/test_prompt.py b/libs/langchain/tests/unit_tests/prompts/test_prompt.py index 7966a3067b9..8e522519495 100644 --- a/libs/langchain/tests/unit_tests/prompts/test_prompt.py +++ b/libs/langchain/tests/unit_tests/prompts/test_prompt.py @@ -177,6 +177,17 @@ Will it get confused{ }? assert prompt == expected_prompt +@pytest.mark.requires("jinja2") +def test_basic_sandboxing_with_jinja2() -> None: + """Test basic sandboxing with jinja2.""" + import jinja2 + + template = " {{''.__class__.__bases__[0] }} " # malicious code + prompt = PromptTemplate.from_template(template, template_format="jinja2") + with pytest.raises(jinja2.exceptions.SecurityError): + assert prompt.format() == [] + + @pytest.mark.requires("jinja2") def test_prompt_from_jinja2_template_multiple_inputs() -> None: """Test with multiple input variables."""