88 lines
3.1 KiB
Diff
88 lines
3.1 KiB
Diff
|
From 065334d1ee5b7210e1a0a93c37238c86858f2af7 Mon Sep 17 00:00:00 2001
|
||
|
From: David Lord <davidism@gmail.com>
|
||
|
Date: Wed, 5 Mar 2025 10:08:48 -0800
|
||
|
Subject: [PATCH] attr filter uses env.getattr
|
||
|
|
||
|
---
|
||
|
src/jinja2/filters.py | 37 ++++++++++++++++---------------------
|
||
|
tests/test_security.py | 10 ++++++++++
|
||
|
2 files changed, 26 insertions(+), 21 deletions(-)
|
||
|
|
||
|
diff --git a/src/jinja2/filters.py b/src/jinja2/filters.py
|
||
|
index e5b5a00c5..2bcba4fbd 100644
|
||
|
--- a/src/jinja2/filters.py
|
||
|
+++ b/src/jinja2/filters.py
|
||
|
@@ -6,6 +6,7 @@
|
||
|
import typing
|
||
|
import typing as t
|
||
|
from collections import abc
|
||
|
+from inspect import getattr_static
|
||
|
from itertools import chain
|
||
|
from itertools import groupby
|
||
|
|
||
|
@@ -1411,31 +1412,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 <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
|
||
|
diff --git a/tests/test_security.py b/tests/test_security.py
|
||
|
index 864d5f7f9..3a1378192 100644
|
||
|
--- a/tests/test_security.py
|
||
|
+++ b/tests/test_security.py
|
||
|
@@ -190,3 +190,13 @@ def run(value, arg):
|
||
|
|
||
|
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()
|