From 065334d1ee5b7210e1a0a93c37238c86858f2af7 Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 5 Mar 2025 10:08:48 -0800 Subject: [PATCH] attr filter uses env.getattr --- Jinja2-3.0.3/CHANGES.rst | 3 +++ Jinja2-3.0.3/src/jinja2/filters.py | 38 ++++++++++++----------------- Jinja2-3.0.3/tests/test_security.py | 11 +++++++++ 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/Jinja2-3.0.3/CHANGES.rst b/Jinja2-3.0.3/CHANGES.rst index 55d8a96..9170467 100644 --- a/Jinja2-3.0.3/CHANGES.rst +++ b/Jinja2-3.0.3/CHANGES.rst @@ -1,5 +1,8 @@ .. currentmodule:: jinja2 +- The ``|attr`` filter does not bypass the environment's attribute lookup, + allowing the sandbox to apply its checks. :ghsa:`cpwx-vrp4-4pq7` + - Escape template name before formatting it into error messages, to avoid issues with names that contain f-string syntax. :issue:`1792`, :ghsa:`gmj6-6f8f-6699` diff --git a/Jinja2-3.0.3/src/jinja2/filters.py b/Jinja2-3.0.3/src/jinja2/filters.py index f3d4fca..0449901 100644 --- a/Jinja2-3.0.3/src/jinja2/filters.py +++ b/Jinja2-3.0.3/src/jinja2/filters.py @@ -6,6 +6,7 @@ import typing import typing as t import warnings from collections import abc +from inspect import getattr_static from itertools import chain from itertools import groupby @@ -1382,32 +1383,25 @@ def do_reverse(value: t.Union[str, t.Iterable[V]]) -> t.Union[str, t.Iterable[V] def do_attr( environment: "Environment", obj: t.Any, name: str ) -> t.Union[Undefined, t.Any]: - """Get an attribute of an object. ``foo|attr("bar")`` works like - ``foo.bar`` just that always an attribute is returned and items are not - looked up. + """Get an attribute of an object. ``foo|attr("bar")`` works like + ``foo.bar``, but returns undefined instead of falling back to ``foo["bar"]`` + if the attribute doesn't exist. See :ref:`Notes on subscriptions ` for more details. """ + # Environment.getattr will fall back to obj[name] if obj.name doesn't exist. + # But we want to call env.getattr to get behavior such as sandboxing. + # Determine if the attr exists first, so we know the fallback won't trigger. try: - name = str(name) - except UnicodeError: - pass - else: - try: - value = getattr(obj, name) - except AttributeError: - pass - else: - if environment.sandboxed: - environment = t.cast("SandboxedEnvironment", environment) - - if not environment.is_safe_attribute(obj, name, value): - return environment.unsafe_undefined(obj, name) - - return value - - return environment.undefined(obj=obj, name=name) - + # This avoids executing properties/descriptors, but misses __getattr__ + # and __getattribute__ dynamic attrs. + getattr_static(obj, name) + except AttributeError: + # This finds dynamic attrs, and we know it's not a descriptor at this point. + if not hasattr(obj, name): + return environment.undefined(obj=obj, name=name) + + return environment.getattr(obj, name) @typing.overload def sync_do_map( diff --git a/Jinja2-3.0.3/tests/test_security.py b/Jinja2-3.0.3/tests/test_security.py index 9c8bad6..323c6b5 100644 --- a/Jinja2-3.0.3/tests/test_security.py +++ b/Jinja2-3.0.3/tests/test_security.py @@ -189,3 +189,14 @@ class TestStringFormatMap: with pytest.raises(SecurityError): t.render() + + + def test_attr_filter(self) -> None: + env = SandboxedEnvironment() + t = env.from_string( + """{{ "{0.__call__.__builtins__[__import__]}" + | attr("format")(not_here) }}""" + ) + + with pytest.raises(SecurityError): + t.render() -- 2.46.0