CVE-2021-42134 (Python, published 2021-10-11T01:15Z)
Description: value: The Unicorn framework before 0.36.1 for Django allows XSS via a component. NOTE: this issue exists because of an incomplete fix for CVE-2021-42053.
Severity: MEDIUM (CVSS v3 base score: 6.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/adamghill/django-unicorn
Files changed:
django_unicorn/components/unicorn_template_response.py
django_unicorn/components/unicorn_view.py
django_unicorn/views/__init__.py
tests/components/test_component.py
tests/components/test_unicorn_template_response.py
tests/templates/test_component_kwargs.html
tests/templates/test_component_kwargs_with_html_entity.html
tests/templates/test_component_variable.html
tests/templatetags/test_unicorn_render.py
tests/views/message/test_sync_input.py
tests/views/test_process_component_request.py
Commit message: More complete handling for preventing XSS attacks.


Vulnerable code:
from django.utils.safestring import mark_safe
        rendered_template = mark_safe(rendered_template)
from django.utils.html import conditional_escape
        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)

        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] = escape(
                    frontend_context_variable_value
                )

from typing import Dict
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>'
    )


from django_unicorn.components.unicorn_template_response import get_root_element
    ->{{ hello }}<-
    assert "->tested!<-" in actual
    assert "->variable!<-" in actual
import orjson


Fixed code:
from bs4.dammit import EntitySubstitution
    def __init__(self):
        super().__init__(entity_substitution=EntitySubstitution.substitute_html)

from typing import Dict, Sequence
from django.utils.safestring import mark_safe
    # Get set of attributes that should be marked as `safe`
    safe_fields = []
    if hasattr(component, "Meta") and hasattr(component.Meta, "safe"):
        if isinstance(component.Meta.safe, Sequence):
            for field_name in component.Meta.safe:
                if field_name in component._attributes().keys():
                    safe_fields.append(field_name)

    # Mark safe attributes as such before rendering
    for field_name in safe_fields:
        value = getattr(component, field_name)
        if isinstance(value, str):
            setattr(component, field_name, mark_safe(value))

from django_unicorn.components.unicorn_template_response import (
    UnicornTemplateResponse,
    get_root_element,
)


def test_desoupify():
    html = "<div>&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;!\n</div>\n\n<script type=\"application/javascript\">\n  window.addEventListener('DOMContentLoaded', (event) => {\n    Unicorn.addEventListener('updated', (component) => console.log('got updated', component));\n  });\n</script>"
    expected = "<div>&lt;a&gt;&lt;style&gt;@keyframes x{}&lt;/style&gt;&lt;a style=\"animation-name:x\" onanimationend=\"alert(1)\"&gt;&lt;/a&gt;!\n</div>\n<script type=\"application/javascript\">\n  window.addEventListener('DOMContentLoaded', (event) => {\n    Unicorn.addEventListener('updated', (component) => console.log('got updated', component));\n  });\n</script>"

    soup = BeautifulSoup(html, "html.parser")

    actual = UnicornTemplateResponse._desoupify(soup)

    assert expected == actual
  <b>{{ hello }}</b>
<div>
  ->{{ hello }}<-
</div>
<div>
  {{ hello }}
</div>
class FakeComponentKwargsWithHtmlEntity(UnicornView):
    template_name = "templates/test_component_kwargs_with_html_entity.html"
    hello = "world"

    def __init__(self, *args, **kwargs):
        super().__init__(**kwargs)
        self.hello = kwargs.get("test_kwarg")


    assert "<b>tested!</b>" in actual
    assert "<b>variable!</b>" in actual


def test_unicorn_render_with_invalid_html():
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargsWithHtmlEntity' test_kwarg=test_var.nested",
    )
    unicorn_node = unicorn(None, token)
    context = {"test_var": {"nested": "variable!"}}
    actual = unicorn_node.render(context)

    assert "-&gt;variable!&lt;-" in actual
from django_unicorn.components import UnicornView
from tests.views.message.utils import post_and_get_response


class FakeComponent(UnicornView):
    template_name = "templates/test_component_variable.html"

    hello = ""


class FakeComponentSafe(UnicornView):
    template_name = "templates/test_component_variable.html"

    hello = ""

    class Meta:
        safe = ("hello",)


def test_html_entities_encoded(client):
    data = {"hello": "test"}
    action_queue = [
        {"payload": {"name": "hello", "value": "<b>test1</b>"}, "type": "syncInput",}
    ]
    response = post_and_get_response(
        client,
        url="/message/tests.views.test_process_component_request.FakeComponent",
        data=data,
        action_queue=action_queue,
    )

    assert not response["errors"]
    assert response["data"].get("hello") == "<b>test1</b>"
    assert "&lt;b&gt;test1&lt;/b&gt;" in response["dom"]


def test_safe_html_entities_not_encoded(client):
    data = {"hello": "test"}
    action_queue = [
        {"payload": {"name": "hello", "value": "<b>test1</b>"}, "type": "syncInput",}
    ]
    response = post_and_get_response(
        client,
        url="/message/tests.views.test_process_component_request.FakeComponentSafe",
        data=data,
        action_queue=action_queue,
    )

    assert not response["errors"]
    assert response["data"].get("hello") == "<b>test1</b>"
    assert "<b>test1</b>" in response["dom"]