From 7be792b4f0d424a8b0458e8bc4acd0fa2a7d8221 Mon Sep 17 00:00:00 2001
From: David Lord <davidism@gmail.com>
Date: Thu, 2 May 2024 09:14:00 -0700
Subject: disallow invalid characters in keys to xmlattr filter

---
 src/jinja2/filters.py | 18 +++++++++++++-----
 tests/test_filters.py | 11 ++++++-----
 2 files changed, 19 insertions(+), 10 deletions(-)

diff --git a/src/jinja2/filters.py b/src/jinja2/filters.py
index 3e07526..ea5678a 100644
--- a/src/jinja2/filters.py
+++ b/src/jinja2/filters.py
@@ -248,7 +248,9 @@ def do_items(value: t.Union[t.Mapping[K, V], Undefined]) -> t.Iterator[t.Tuple[K
     yield from value.items()
 
 
-_space_re = re.compile(r"\s", flags=re.ASCII)
+# Check for characters that would move the parser state from key to value.
+# https://html.spec.whatwg.org/#attribute-name-state
+_attr_key_re = re.compile(r"[\s/>=]", flags=re.ASCII)
 
 
 @pass_eval_context
@@ -257,8 +259,14 @@ def do_xmlattr(
 ) -> str:
     """Create an SGML/XML attribute string based on the items in a dict.
 
-    If any key contains a space, this fails with a ``ValueError``. Values that
-    are neither ``none`` nor ``undefined`` are automatically escaped.
+    **Values** that are neither ``none`` nor ``undefined`` are automatically
+    escaped, safely allowing untrusted user input.
+
+    User input should not be used as **keys** to this filter. If any key
+    contains a space, ``/`` solidus, ``>`` greater-than sign, or ``=`` equals
+    sign, this fails with a ``ValueError``. Regardless of this, user input
+    should never be used as keys to this filter, or must be separately validated
+    first.
 
     .. sourcecode:: html+jinja
 
@@ -284,8 +292,8 @@ def do_xmlattr(
         if value is None or isinstance(value, Undefined):
             continue
 
-        if _space_re.search(key) is not None:
-            raise ValueError(f"Spaces are not allowed in attributes: '{key}'")
+        if _attr_key_re.search(key) is not None:
+            raise ValueError(f"Invalid character in attribute name: {key!r}")
 
         items.append(f'{escape(key)}="{escape(value)}"')
 
diff --git a/tests/test_filters.py b/tests/test_filters.py
index a184649..c9ec7da 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -474,11 +474,12 @@ class TestFilter:
         assert 'bar="23"' in out
         assert 'blub:blub="&lt;?&gt;"' in out
 
-    def test_xmlattr_key_with_spaces(self, env):
-        with pytest.raises(ValueError, match="Spaces are not allowed"):
-            env.from_string(
-                "{{ {'src=1 onerror=alert(1)': 'my_class'}|xmlattr }}"
-            ).render()
+    @pytest.mark.parametrize("sep", ("\t", "\n", "\f", " ", "/", ">", "="))
+    def test_xmlattr_key_invalid(self, env: Environment, sep: str) -> None:
+        with pytest.raises(ValueError, match="Invalid character"):
+            env.from_string("{{ {key: 'my_class'}|xmlattr }}").render(
+                key=f"class{sep}onclick=alert(1)"
+            )
 
     def test_sort1(self, env):
         tmpl = env.from_string("{{ [2, 3, 1]|sort }}|{{ [2, 3, 1]|sort(true) }}")
-- 
2.30.2

