1 | """
|
---|
2 | Proof of Concept for an XSS on the Technical 500 debug page.
|
---|
3 |
|
---|
4 | Alternate take on it. Still requires a misunderstanding by a user, marking
|
---|
5 | something as safe when in fact it's _not_.
|
---|
6 | """
|
---|
7 | import django
|
---|
8 | from django import template
|
---|
9 | from django.conf import settings
|
---|
10 | from django.http import HttpResponse
|
---|
11 | from django.template import Template, Context
|
---|
12 | from django.urls import path
|
---|
13 | from django.utils.safestring import mark_safe
|
---|
14 |
|
---|
15 | if not settings.configured:
|
---|
16 | settings.configure(
|
---|
17 | SECRET_KEY="??????????????????????????????????????????????????????????",
|
---|
18 | DEBUG=True,
|
---|
19 | INSTALLED_APPS=(),
|
---|
20 | ALLOWED_HOSTS=("*"),
|
---|
21 | ROOT_URLCONF=__name__,
|
---|
22 | MIDDLEWARE=[],
|
---|
23 | TEMPLATES=[
|
---|
24 | {
|
---|
25 | "BACKEND": "django.template.backends.django.DjangoTemplates",
|
---|
26 | "DIRS": [],
|
---|
27 | "APP_DIRS": False,
|
---|
28 | "OPTIONS": {
|
---|
29 | "context_processors": [],
|
---|
30 | "libraries": {
|
---|
31 | "my_custom_filters": __name__,
|
---|
32 | },
|
---|
33 | },
|
---|
34 | },
|
---|
35 | ],
|
---|
36 | )
|
---|
37 | django.setup()
|
---|
38 |
|
---|
39 |
|
---|
40 | class MyModel:
|
---|
41 | def get_user_value1(self):
|
---|
42 | # User controlled data, but for some reason has been marked as safe.
|
---|
43 | # Always risky! But I need a way to provide a SafeString to the
|
---|
44 | # filter.
|
---|
45 | # Perhaps I've copied it from production to investigate what's going on.
|
---|
46 | return mark_safe("<script>alert('good')</script>")
|
---|
47 |
|
---|
48 | def get_user_value2(self):
|
---|
49 | return mark_safe("<script>alert('bad')</script>")
|
---|
50 |
|
---|
51 |
|
---|
52 | register = template.Library()
|
---|
53 |
|
---|
54 |
|
---|
55 | @register.filter("test_filter")
|
---|
56 | def test_filter(val, arg):
|
---|
57 | """
|
---|
58 | Another contrived thing...
|
---|
59 | If I can get user controlled data to become a SafeString, and force
|
---|
60 | an exception to occur with the value as args[0]
|
---|
61 | """
|
---|
62 |
|
---|
63 | # This is where 'the magic' happens; if .partition (or 'rpartition') don't
|
---|
64 | # find the separator, they return the whole original(!) value as [0] (or [2])
|
---|
65 | #
|
---|
66 | # The only other method I can find to not actually coerce to a string is
|
---|
67 | # SafeString('...').format(x=x) where format does no replacements, and
|
---|
68 | # f'{mysafestring}' where the f-string has literally no other characters...
|
---|
69 | parts = list(arg.partition("'good'")) # I could be splitting by anything.
|
---|
70 |
|
---|
71 | # I'm a person who doesn't want things to silently fail in dev, because
|
---|
72 | # then they'll silently fail in production.
|
---|
73 | # (I actually _am_ that person it turns out, given I wrote
|
---|
74 | # https://github.com/kezabelle/django-shouty-templates to avoid that
|
---|
75 | # happening. I would be raising an exception, but probably not quite like
|
---|
76 | # this.)
|
---|
77 | if settings.DEBUG and not parts[2]:
|
---|
78 | # I'm for some reason choosing to rely on exceptions accepting varargs
|
---|
79 | # and binding those into .args - this gets me args[0] as a SafeString
|
---|
80 | # Note that the asterisk expansion is required, even though the str()
|
---|
81 | # output looks the same!
|
---|
82 | raise ValueError(*parts)
|
---|
83 | raise ValueError(arg) # This would also work, I guess.
|
---|
84 | raise ValueError(f'{arg}') # As would this ...
|
---|
85 | # Here I might be mutating the value, which is fine because the act of
|
---|
86 | # combining a str() + SafeString() leads to a str(). So it needs marking
|
---|
87 | # as safe again in the template...
|
---|
88 | parts.insert(0, val)
|
---|
89 | return "".join(parts)
|
---|
90 |
|
---|
91 |
|
---|
92 | def via_filter_looks_safe(request) -> HttpResponse:
|
---|
93 | template = Template(
|
---|
94 | """
|
---|
95 | {% load my_custom_filters %}
|
---|
96 | {{ 'safe text or variable'|test_filter:object.get_user_value1 }}
|
---|
97 | """
|
---|
98 | )
|
---|
99 | return HttpResponse(template.render(Context({
|
---|
100 | "object": MyModel(),
|
---|
101 | })))
|
---|
102 |
|
---|
103 |
|
---|
104 | def via_filter(request) -> HttpResponse:
|
---|
105 | template = Template(
|
---|
106 | """
|
---|
107 | {% load my_custom_filters %}
|
---|
108 | {{ 'safe text or variable'|test_filter:object.get_user_value2 }}
|
---|
109 | """
|
---|
110 | )
|
---|
111 |
|
---|
112 | return HttpResponse(template.render(Context({
|
---|
113 | "object": MyModel(),
|
---|
114 | })))
|
---|
115 |
|
---|
116 |
|
---|
117 | urlpatterns = [
|
---|
118 | path("good", via_filter_looks_safe),
|
---|
119 | path("bad", via_filter),
|
---|
120 | ]
|
---|
121 |
|
---|
122 |
|
---|
123 | if __name__ == "__main__":
|
---|
124 | from django.core import management
|
---|
125 |
|
---|
126 | management.execute_from_command_line()
|
---|
127 | else:
|
---|
128 | from django.core.wsgi import get_wsgi_application
|
---|
129 |
|
---|
130 | application = get_wsgi_application()
|
---|