summaryrefslogtreecommitdiff
path: root/backport-CVE-2024-56326.patch
diff options
context:
space:
mode:
Diffstat (limited to 'backport-CVE-2024-56326.patch')
-rw-r--r--backport-CVE-2024-56326.patch172
1 files changed, 172 insertions, 0 deletions
diff --git a/backport-CVE-2024-56326.patch b/backport-CVE-2024-56326.patch
new file mode 100644
index 0000000..6b3394e
--- /dev/null
+++ b/backport-CVE-2024-56326.patch
@@ -0,0 +1,172 @@
+From 91a972f5808973cd441f4dc06873b2f8378f30c7 Mon Sep 17 00:00:00 2001
+From: Lydxn <hlyndon20@gmail.com>
+Date: Mon, 23 Sep 2024 15:09:10 -0700
+Subject: [PATCH] sandbox indirect calls to str.format
+---
+ Jinja2-3.1.3/src/jinja2/sandbox.py | 81 +++++++++++++++--------------
+ Jinja2-3.1.3/tests/test_security.py | 18 +++++++
+ 2 files changed, 60 insertions(+), 39 deletions(-)
+
+diff --git a/Jinja2-3.1.3/src/jinja2/sandbox.py b/Jinja2-3.1.3/src/jinja2/sandbox.py
+index 06d7414..432d28f 100644
+--- a/Jinja2-3.1.3/src/jinja2/sandbox.py
++++ b/Jinja2-3.1.3/src/jinja2/sandbox.py
+@@ -7,6 +7,7 @@ import typing as t
+ from _string import formatter_field_name_split # type: ignore
+ from collections import abc
+ from collections import deque
++from functools import update_wrapper
+ from string import Formatter
+
+ from markupsafe import EscapeFormatter
+@@ -79,21 +80,6 @@ _mutable_spec: t.Tuple[t.Tuple[t.Type, t.FrozenSet[str]], ...] = (
+ ),
+ )
+
+-
+-def inspect_format_method(callable: t.Callable) -> t.Optional[str]:
+- if not isinstance(
+- callable, (types.MethodType, types.BuiltinMethodType)
+- ) or callable.__name__ not in ("format", "format_map"):
+- return None
+-
+- obj = callable.__self__
+-
+- if isinstance(obj, str):
+- return obj
+-
+- return None
+-
+-
+ def safe_range(*args: int) -> range:
+ """A range that can't generate ranges with a length of more than
+ MAX_RANGE items.
+@@ -313,6 +299,9 @@ class SandboxedEnvironment(Environment):
+ except AttributeError:
+ pass
+ else:
++ fmt = self.wrap_str_format(value)
++ if fmt is not None:
++ return fmt
+ if self.is_safe_attribute(obj, argument, value):
+ return value
+ return self.unsafe_undefined(obj, argument)
+@@ -330,6 +319,9 @@ class SandboxedEnvironment(Environment):
+ except (TypeError, LookupError):
+ pass
+ else:
++ fmt = self.wrap_str_format(value)
++ if fmt is not None:
++ return fmt
+ if self.is_safe_attribute(obj, attribute, value):
+ return value
+ return self.unsafe_undefined(obj, attribute)
+@@ -345,34 +337,48 @@ class SandboxedEnvironment(Environment):
+ exc=SecurityError,
+ )
+
+- def format_string(
+- self,
+- s: str,
+- args: t.Tuple[t.Any, ...],
+- kwargs: t.Dict[str, t.Any],
+- format_func: t.Optional[t.Callable] = None,
+- ) -> str:
+- """If a format call is detected, then this is routed through this
+- method so that our safety sandbox can be used for it.
++ def wrap_str_format(self, value: t.Any) -> t.Optional[t.Callable[..., str]]:
++ """If the given value is a ``str.format`` or ``str.format_map`` method,
++ return a new function than handles sandboxing. This is done at access
++ rather than in :meth:`call`, so that calls made without ``call`` are
++ also sandboxed.
+ """
++ if not isinstance(
++ value, (types.MethodType, types.BuiltinMethodType)
++ ) or value.__name__ not in ("format", "format_map"):
++ return None
++
++ f_self: t.Any = value.__self__
++
++ if not isinstance(f_self, str):
++ return None
++
++ str_type: t.Type[str] = type(f_self)
++ is_format_map = value.__name__ == "format_map"
+ formatter: SandboxedFormatter
+- if isinstance(s, Markup):
+- formatter = SandboxedEscapeFormatter(self, escape=s.escape)
++
++ if isinstance(f_self, Markup):
++ formatter = SandboxedEscapeFormatter(self, escape=f_self.escape)
+ else:
+ formatter = SandboxedFormatter(self)
+
+- if format_func is not None and format_func.__name__ == "format_map":
+- if len(args) != 1 or kwargs:
+- raise TypeError(
+- "format_map() takes exactly one argument"
+- f" {len(args) + (kwargs is not None)} given"
+- )
++ vformat = formatter.vformat
++
++ def wrapper(*args: t.Any, **kwargs: t.Any) -> str:
++ if is_format_map:
++ if kwargs:
++ raise TypeError("format_map() takes no keyword arguments")
++
++ if len(args) != 1:
++ raise TypeError(
++ f"format_map() takes exactly one argument ({len(args)} given)"
++ )
+
+- kwargs = args[0]
+- args = ()
++ kwargs = args[0]
++ args = ()
+
+- rv = formatter.vformat(s, args, kwargs)
+- return type(s)(rv)
++ return str_type(vformat(f_self, args, kwargs))
++ return update_wrapper(wrapper, value)
+
+ def call(
+ __self, # noqa: B902
+@@ -382,9 +388,6 @@ class SandboxedEnvironment(Environment):
+ **kwargs: t.Any,
+ ) -> t.Any:
+ """Call an object from sandboxed code."""
+- fmt = inspect_format_method(__obj)
+- if fmt is not None:
+- return __self.format_string(fmt, args, kwargs, __obj)
+
+ # the double prefixes are to avoid double keyword argument
+ # errors when proxying the call.
+diff --git a/Jinja2-3.1.3/tests/test_security.py b/Jinja2-3.1.3/tests/test_security.py
+index 0e8dc5c..9c8bad6 100644
+--- a/Jinja2-3.1.3/tests/test_security.py
++++ b/Jinja2-3.1.3/tests/test_security.py
+@@ -171,3 +171,21 @@ class TestStringFormatMap:
+ '{{ ("a{x.foo}b{y}"|safe).format_map({"x":{"foo": 42}, "y":"<foo>"}) }}'
+ )
+ assert t.render() == "a42b&lt;foo&gt;"
++
++
++ def test_indirect_call(self):
++ def run(value, arg):
++ return value.run(arg)
++
++ env = SandboxedEnvironment()
++ env.filters["run"] = run
++ t = env.from_string(
++ """{% set
++ ns = namespace(run="{0.__call__.__builtins__[__import__]}".format)
++ %}
++ {{ ns | run(not_here) }}
++ """
++ )
++
++ with pytest.raises(SecurityError):
++ t.render()
+--
+2.43.0
+