Skip to content

Commit 16355a9

Browse files
committed
✅ [open-formulieren/security-issues#35] Testing svg sanitizer
1 parent ae4792b commit 16355a9

File tree

3 files changed

+191
-1
lines changed

3 files changed

+191
-1
lines changed

src/openforms/config/tests/test_global_configuration.py

+51-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import clamd
1313
from django_webtest import WebTest
1414
from maykin_2fa.test import disable_admin_mfa
15-
from webtest import Form as WebTestForm
15+
from webtest import Form as WebTestForm, Upload
1616

1717
from openforms.accounts.tests.factories import SuperUserFactory
1818
from openforms.tests.utils import NOOP_CACHES
@@ -165,6 +165,56 @@ def test_virus_scan_enabled_cant_connect(self):
165165
"Cannot connect to ClamAV: Cannot connect!",
166166
)
167167

168+
@override_settings(LANGUAGE_CODE="en")
169+
def test_svg_sanitation_favicon(self):
170+
bad_svg_content = (
171+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
172+
'<circle cx="25" cy="25" r="25" fill="green" onclick="alert(\'forget about me?\')" />'
173+
"<script>//<![CDATA["
174+
'alert("I am malicious >:)")'
175+
"//]]></script>"
176+
"<g>"
177+
'<rect class="btn" x="0" y="0" width="10" height="10" fill="red" onload="alert(\'click!\')" />'
178+
"</g>"
179+
"</svg>"
180+
)
181+
sanitized_svg_content = (
182+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
183+
'<circle cx="25" cy="25" r="25" fill="green"></circle>'
184+
"//"
185+
'alert("I am malicious &gt;:)")'
186+
"//"
187+
"<g>"
188+
'<rect x="0" y="0" width="10" height="10" fill="red"></rect>'
189+
"</g>"
190+
"</svg>"
191+
)
192+
193+
config = GlobalConfiguration.get_solo()
194+
195+
with self.subTest(part="admin config"):
196+
url = reverse("admin:config_globalconfiguration_change", args=(1,))
197+
change_page = self.app.get(url)
198+
199+
upload = Upload(
200+
"bad_svg.svg", bad_svg_content.encode("utf-8"), "image/svg+xml"
201+
)
202+
203+
form = change_page.forms["globalconfiguration_form"]
204+
form["favicon"] = upload
205+
_ensure_arrayfields(form, config=config)
206+
response = form.submit()
207+
208+
self.assertEqual(response.status_code, 302)
209+
config.refresh_from_db()
210+
self.assertEqual(config.favicon, "logo/bad_svg.svg")
211+
212+
with self.subTest(part="assert favicon sanitized"):
213+
with config.favicon.file.open("rb") as favicon_file:
214+
# Assert that the logo is completely sanitized
215+
decoded_logo = favicon_file.read().decode("utf-8")
216+
self.assertEqual(decoded_logo, sanitized_svg_content)
217+
168218

169219
class GlobalConfirmationEmailTests(TestCase):
170220
def setUp(self):

src/openforms/config/tests/test_theme.py

+48
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,54 @@ def test_upload_svg(self):
7777
style_tag.text(),
7878
)
7979

80+
def test_upload_malicious_svg(self):
81+
bad_svg_content = (
82+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
83+
'<circle cx="25" cy="25" r="25" fill="green" onclick="alert(\'forget about me?\')" />'
84+
"<script>//<![CDATA["
85+
'alert("I am malicious >:)")'
86+
"//]]></script>"
87+
"<g>"
88+
'<rect class="btn" x="0" y="0" width="10" height="10" fill="red" onload="alert(\'click!\')" />'
89+
"</g>"
90+
"</svg>"
91+
)
92+
sanitized_svg_content = (
93+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
94+
'<circle cx="25" cy="25" r="25" fill="green"></circle>'
95+
"//"
96+
'alert("I am malicious &gt;:)")'
97+
"//"
98+
"<g>"
99+
'<rect x="0" y="0" width="10" height="10" fill="red"></rect>'
100+
"</g>"
101+
"</svg>"
102+
)
103+
theme = ThemeFactory.create()
104+
105+
with self.subTest(part="admin config"):
106+
url = reverse("admin:config_theme_change", args=(theme.pk,))
107+
108+
change_page = self.app.get(url)
109+
110+
upload = Upload(
111+
"bad_svg.svg", bad_svg_content.encode("utf-8"), "image/svg+xml"
112+
)
113+
114+
form = change_page.forms["theme_form"]
115+
form["logo"] = upload
116+
response = form.submit()
117+
118+
self.assertEqual(response.status_code, 302)
119+
theme.refresh_from_db()
120+
self.assertEqual(theme.logo, "logo/bad_svg.svg")
121+
122+
with self.subTest(part="assert logo sanitized"):
123+
with theme.logo.file.open("rb") as logo_file:
124+
# Assert that the logo is completely sanitized
125+
decoded_logo = logo_file.read().decode("utf-8")
126+
self.assertEqual(decoded_logo, sanitized_svg_content)
127+
80128
def test_upload_png(self):
81129
logo = Path(settings.DJANGO_PROJECT_DIR) / "static" / "img" / "digid.png"
82130
theme = ThemeFactory.create()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import tempfile
2+
3+
from django.test import TestCase
4+
5+
from ..sanitizer import sanitize_svg_content, sanitize_svg_file
6+
7+
8+
class SanitizeSvgFileTests(TestCase):
9+
def test_sanitize_svg_content_removes_script_tags(self):
10+
bad_svg_content = (
11+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
12+
'<circle cx="25" cy="25" r="25" fill="green" />'
13+
"<script>//<![CDATA["
14+
'alert("I am malicious >:)")'
15+
"//]]></script>"
16+
"<g>"
17+
'<rect class="btn" x="0" y="0" width="10" height="10" fill="red" />'
18+
"</g>"
19+
"</svg>"
20+
)
21+
sanitized_svg_content = (
22+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
23+
'<circle cx="25" cy="25" r="25" fill="green"></circle>'
24+
"//"
25+
'alert("I am malicious &gt;:)")'
26+
"//"
27+
"<g>"
28+
'<rect x="0" y="0" width="10" height="10" fill="red"></rect>'
29+
"</g>"
30+
"</svg>"
31+
)
32+
33+
with tempfile.TemporaryFile() as temp_file:
34+
temp_file.write(bad_svg_content.encode("utf-8"))
35+
36+
# Assert that sanitize_svg_content removed the script tag
37+
sanitized_svg_file = sanitize_svg_file(temp_file)
38+
self.assertEqual(
39+
sanitized_svg_file.read().decode("utf-8"), sanitized_svg_content
40+
)
41+
42+
43+
class SanitizeSvgContentTests(TestCase):
44+
def test_sanitize_svg_content_removes_script_tags(self):
45+
bad_svg_content = (
46+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
47+
'<circle cx="25" cy="25" r="25" fill="green" />'
48+
"<script>//<![CDATA["
49+
'alert("I am malicious >:)")'
50+
"//]]></script>"
51+
"<g>"
52+
'<rect class="btn" x="0" y="0" width="10" height="10" fill="red" />'
53+
"</g>"
54+
"</svg>"
55+
)
56+
sanitized_svg_content = (
57+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
58+
'<circle cx="25" cy="25" r="25" fill="green"></circle>'
59+
"//"
60+
'alert("I am malicious &gt;:)")'
61+
"//"
62+
"<g>"
63+
'<rect x="0" y="0" width="10" height="10" fill="red"></rect>'
64+
"</g>"
65+
"</svg>"
66+
)
67+
68+
# Assert that sanitize_svg_content removed the script tag
69+
sanitized_bad_svg_content = sanitize_svg_content(bad_svg_content)
70+
self.assertEqual(sanitized_svg_content, sanitized_bad_svg_content)
71+
72+
def test_sanitize_svg_content_removes_event_handlers(self):
73+
bad_svg_content = (
74+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
75+
'<circle cx="25" cy="25" r="25" fill="green" onclick="alert(\'forget about me?\')" />'
76+
"<g>"
77+
'<rect class="btn" x="0" y="0" width="10" height="10" fill="red" onload="alert(\'click!\')" />'
78+
"</g>"
79+
"</svg>"
80+
)
81+
sanitized_svg_content = (
82+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
83+
'<circle cx="25" cy="25" r="25" fill="green"></circle>'
84+
"<g>"
85+
'<rect x="0" y="0" width="10" height="10" fill="red"></rect>'
86+
"</g>"
87+
"</svg>"
88+
)
89+
90+
# Assert that sanitize_svg_content removed the event handlers
91+
sanitized_bad_svg_content = sanitize_svg_content(bad_svg_content)
92+
self.assertEqual(sanitized_svg_content, sanitized_bad_svg_content)

0 commit comments

Comments
 (0)