Showing with 73 additions and 6 deletions.
  1. +10 −0 .all-contributorsrc
  2. +2 −1 README.md
  3. +22 −1 django_unicorn/components/unicorn_view.py
  4. +1 −1 django_unicorn/utils.py
  5. +1 −1 pyproject.toml
  6. +34 −1 tests/components/test_component.py
  7. +3 −1 tests/components/test_get_locations.py
10 changes: 10 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@
"contributions": [
"code"
]
},
{
"login": "apoorvaeternity",
"name": "Apoorva Pandey",
"avatar_url": "https://avatars.githubusercontent.com/u/21103831?v=4",
"profile": "http://www.apoorvapandey.com",
"contributions": [
"test",
"code"
]
}
],
"contributorsPerLine": 7,
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
![Sponsors](https://www.sponsorama.dev/api/button/sponsor-count/adamghill)

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-12-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->

[Unicorn](https://www.django-unicorn.com) is a reactive component framework that progressively enhances a normal Django view, makes AJAX calls in the background, and dynamically updates the DOM. It seamlessly extends Django past its server-side framework roots without giving up all of its niceties or re-building your website.
Expand Down Expand Up @@ -49,6 +49,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/Franziskhan"><img src="https://avatars.githubusercontent.com/u/86062014?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Franziskhan</b></sub></a><br /><a href="https://github.com/adamghill/django-unicorn/commits?author=Franziskhan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/joshiggins"><img src="https://avatars.githubusercontent.com/u/5124298?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Josh Higgins</b></sub></a><br /><a href="https://github.com/adamghill/django-unicorn/commits?author=joshiggins" title="Tests">⚠️</a> <a href="https://github.com/adamghill/django-unicorn/commits?author=joshiggins" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/MayasMess"><img src="https://avatars.githubusercontent.com/u/51958712?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Amayas Messara</b></sub></a><br /><a href="https://github.com/adamghill/django-unicorn/commits?author=MayasMess" title="Code">💻</a></td>
<td align="center"><a href="http://www.apoorvapandey.com"><img src="https://avatars.githubusercontent.com/u/21103831?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Apoorva Pandey</b></sub></a><br /><a href="https://github.com/adamghill/django-unicorn/commits?author=apoorvaeternity" title="Tests">⚠️</a> <a href="https://github.com/adamghill/django-unicorn/commits?author=apoorvaeternity" title="Code">💻</a></td>
</tr>
</table>

Expand Down
23 changes: 22 additions & 1 deletion django_unicorn/components/unicorn_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Model
from django.http import HttpRequest
from django.utils.html import conditional_escape
from django.views.generic.base import TemplateView

from cachetools.lru import LRUCache
Expand Down Expand Up @@ -333,13 +334,21 @@ def get_frontend_context_variables(self) -> str:
attributes = self._attributes()
frontend_context_variables.update(attributes)

# Remove any field in `javascript_exclude` from the `frontend_context_variables`
# Remove any field in `javascript_exclude` from `frontend_context_variables`
if hasattr(self, "Meta") and hasattr(self.Meta, "javascript_exclude"):
if isinstance(self.Meta.javascript_exclude, Sequence):
for field_name in self.Meta.javascript_exclude:
if field_name in frontend_context_variables:
del frontend_context_variables[field_name]

safe_fields = []
# Keep a list of fields that are safe to not sanitize from `frontend_context_variables`
if hasattr(self, "Meta") and hasattr(self.Meta, "safe"):
if isinstance(self.Meta.safe, Sequence):
for field_name in self.Meta.safe:
if field_name in frontend_context_variables:
safe_fields.append(field_name)

# Add cleaned values to `frontend_content_variables` based on the widget in form's fields
form = self._get_form(attributes)

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

for (
frontend_context_variable_key,
frontend_context_variable_value,
) in frontend_context_variables.items():
if (
isinstance(frontend_context_variable_value, str)
and frontend_context_variable_key not in safe_fields
):
frontend_context_variables[
frontend_context_variable_key
] = conditional_escape(frontend_context_variable_value)

encoded_frontend_context_variables = serializer.dumps(
frontend_context_variables
)
Expand Down
2 changes: 1 addition & 1 deletion django_unicorn/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import get_type_hints as typing_get_type_hints

from django.conf import settings
from django.utils.html import _json_script_escapes, format_html
from django.utils.html import _json_script_escapes
from django.utils.safestring import mark_safe

import shortuuid
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-unicorn"
version = "0.35.2"
version = "0.36.0"
description = "A magical full-stack framework for Django."
authors = ["Adam Hill <unicorn@adamghill.com>"]
license = "MIT"
Expand Down
35 changes: 34 additions & 1 deletion tests/components/test_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def get_name(self):
return "World"


@pytest.fixture(scope="module")
@pytest.fixture()
def component():
return ExampleComponent(component_id="asdf1234", component_name="example")

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


def test_get_frontend_context_variables_xss(component):
# Set component.name to a potential XSS attack
component.name = '<a><style>@keyframes x{}</style><a style="animation-name:x" onanimationend="alert(1)"></a>'

frontend_context_variables = component.get_frontend_context_variables()
frontend_context_variables_dict = orjson.loads(frontend_context_variables)
assert len(frontend_context_variables_dict) == 1
assert (
frontend_context_variables_dict.get("name")
== "&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;"
)


def test_get_frontend_context_variables_safe(component):
# Set component.name to a potential XSS attack
component.name = '<a><style>@keyframes x{}</style><a style="animation-name:x" onanimationend="alert(1)"></a>'

class Meta:
safe = [
"name",
]

setattr(component, "Meta", Meta())

frontend_context_variables = component.get_frontend_context_variables()
frontend_context_variables_dict = orjson.loads(frontend_context_variables)
assert len(frontend_context_variables_dict) == 1
assert (
frontend_context_variables_dict.get("name")
== '<a><style>@keyframes x{}</style><a style="animation-name:x" onanimationend="alert(1)"></a>'
)


def test_get_context_data(component):
context_data = component.get_context_data()
assert (
Expand Down
4 changes: 3 additions & 1 deletion tests/components/test_get_locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ def test_get_locations_apps_setting_invalid(settings):
def test_get_locations_installed_app_with_app_config(settings):
unicorn_apps = settings.UNICORN["APPS"]
del settings.UNICORN["APPS"]
settings.INSTALLED_APPS = ["example.coffee.apps.Config",]
settings.INSTALLED_APPS = [
"example.coffee.apps.Config",
]

expected = [("HelloWorldView", "example.coffee.components.hello_world",)]
actual = get_locations("hello-world")
Expand Down