Skip to content

Commit aa5b983

Browse files
committed
Escape component values.
1 parent b11b4c1 commit aa5b983

File tree

3 files changed

+57
-3
lines changed

3 files changed

+57
-3
lines changed

django_unicorn/components/unicorn_view.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from django.core.exceptions import ImproperlyConfigured
1111
from django.db.models import Model
1212
from django.http import HttpRequest
13+
from django.utils.html import conditional_escape
1314
from django.views.generic.base import TemplateView
1415

1516
from cachetools.lru import LRUCache
@@ -333,13 +334,21 @@ def get_frontend_context_variables(self) -> str:
333334
attributes = self._attributes()
334335
frontend_context_variables.update(attributes)
335336

336-
# Remove any field in `javascript_exclude` from the `frontend_context_variables`
337+
# Remove any field in `javascript_exclude` from `frontend_context_variables`
337338
if hasattr(self, "Meta") and hasattr(self.Meta, "javascript_exclude"):
338339
if isinstance(self.Meta.javascript_exclude, Sequence):
339340
for field_name in self.Meta.javascript_exclude:
340341
if field_name in frontend_context_variables:
341342
del frontend_context_variables[field_name]
342343

344+
safe_fields = []
345+
# Keep a list of fields that are safe to not sanitize from `frontend_context_variables`
346+
if hasattr(self, "Meta") and hasattr(self.Meta, "safe"):
347+
if isinstance(self.Meta.safe, Sequence):
348+
for field_name in self.Meta.safe:
349+
if field_name in frontend_context_variables:
350+
safe_fields.append(field_name)
351+
343352
# Add cleaned values to `frontend_content_variables` based on the widget in form's fields
344353
form = self._get_form(attributes)
345354

@@ -363,6 +372,18 @@ def get_frontend_context_variables(self) -> str:
363372
):
364373
frontend_context_variables[key] = value
365374

375+
for (
376+
frontend_context_variable_key,
377+
frontend_context_variable_value,
378+
) in frontend_context_variables.items():
379+
if (
380+
isinstance(frontend_context_variable_value, str)
381+
and frontend_context_variable_key not in safe_fields
382+
):
383+
frontend_context_variables[
384+
frontend_context_variable_key
385+
] = conditional_escape(frontend_context_variable_value)
386+
366387
encoded_frontend_context_variables = serializer.dumps(
367388
frontend_context_variables
368389
)

django_unicorn/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import get_type_hints as typing_get_type_hints
77

88
from django.conf import settings
9-
from django.utils.html import _json_script_escapes, format_html
9+
from django.utils.html import _json_script_escapes
1010
from django.utils.safestring import mark_safe
1111

1212
import shortuuid

tests/components/test_component.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def get_name(self):
1313
return "World"
1414

1515

16-
@pytest.fixture(scope="module")
16+
@pytest.fixture()
1717
def component():
1818
return ExampleComponent(component_id="asdf1234", component_name="example")
1919

@@ -82,6 +82,39 @@ def test_get_frontend_context_variables(component):
8282
assert frontend_context_variables_dict.get("name") == "World"
8383

8484

85+
def test_get_frontend_context_variables_xss(component):
86+
# Set component.name to a potential XSS attack
87+
component.name = '<a><style>@keyframes x{}</style><a style="animation-name:x" onanimationend="alert(1)"></a>'
88+
89+
frontend_context_variables = component.get_frontend_context_variables()
90+
frontend_context_variables_dict = orjson.loads(frontend_context_variables)
91+
assert len(frontend_context_variables_dict) == 1
92+
assert (
93+
frontend_context_variables_dict.get("name")
94+
== "&lt;a&gt;&lt;style&gt;@keyframes x{}&lt;/style&gt;&lt;a style=&quot;animation-name:x&quot; onanimationend=&quot;alert(1)&quot;&gt;&lt;/a&gt;"
95+
)
96+
97+
98+
def test_get_frontend_context_variables_safe(component):
99+
# Set component.name to a potential XSS attack
100+
component.name = '<a><style>@keyframes x{}</style><a style="animation-name:x" onanimationend="alert(1)"></a>'
101+
102+
class Meta:
103+
safe = [
104+
"name",
105+
]
106+
107+
setattr(component, "Meta", Meta())
108+
109+
frontend_context_variables = component.get_frontend_context_variables()
110+
frontend_context_variables_dict = orjson.loads(frontend_context_variables)
111+
assert len(frontend_context_variables_dict) == 1
112+
assert (
113+
frontend_context_variables_dict.get("name")
114+
== '<a><style>@keyframes x{}</style><a style="animation-name:x" onanimationend="alert(1)"></a>'
115+
)
116+
117+
85118
def test_get_context_data(component):
86119
context_data = component.get_context_data()
87120
assert (

0 commit comments

Comments
 (0)