Skip to content

Commit e923789

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

File tree

5 files changed

+156
-1
lines changed

5 files changed

+156
-1
lines changed
Loading
Loading

src/openforms/config/tests/test_global_configuration.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import shutil
33
import tempfile
4+
from pathlib import Path
45
from unittest.mock import patch
56

67
from django.conf import settings
@@ -12,14 +13,17 @@
1213
import clamd
1314
from django_webtest import WebTest
1415
from maykin_2fa.test import disable_admin_mfa
15-
from webtest import Form as WebTestForm
16+
from webtest import Form as WebTestForm, Upload
1617

1718
from openforms.accounts.tests.factories import SuperUserFactory
1819
from openforms.tests.utils import NOOP_CACHES
1920

2021
from ..models import GlobalConfiguration
2122

2223

24+
PATH = Path(__file__).parent
25+
26+
2327
def _ensure_arrayfields(form: WebTestForm, config: GlobalConfiguration | None = None):
2428
if config is None:
2529
config = GlobalConfiguration.get_solo() # type: ignore
@@ -165,6 +169,45 @@ def test_virus_scan_enabled_cant_connect(self):
165169
"Cannot connect to ClamAV: Cannot connect!",
166170
)
167171

172+
@override_settings(LANGUAGE_CODE="en")
173+
def test_svg_sanitation_favicon(self):
174+
config = GlobalConfiguration.get_solo()
175+
bad_svg = PATH / "data" / "bad_svg.svg"
176+
sanitized_svg = PATH / "data" / "sanitized_svg.svg"
177+
178+
with self.subTest(part="admin config"):
179+
url = reverse("admin:config_globalconfiguration_change", args=(1,))
180+
181+
change_page = self.app.get(url)
182+
183+
with open(bad_svg, "rb") as bad_file:
184+
# Assert that the bad svg contains script tags, which aren't allowed
185+
decoded_svg = bad_file.read().decode("utf-8")
186+
self.assertIn("<script", decoded_svg)
187+
bad_file.seek(0)
188+
189+
upload = Upload("bad_svg.svg", bad_file.read(), "image/svg+xml")
190+
191+
form = change_page.forms["globalconfiguration_form"]
192+
form["favicon"] = upload
193+
_ensure_arrayfields(form, config=config)
194+
response = form.submit()
195+
196+
self.assertEqual(response.status_code, 302)
197+
config.refresh_from_db()
198+
self.assertEqual(config.favicon, "logo/bad_svg.svg")
199+
200+
with self.subTest(part="assert favicon sanitized"):
201+
with config.favicon.file.open("rb") as favicon_file:
202+
# Assert that the logo has been sanitized and doesn't contain script tags
203+
decoded_logo = favicon_file.read().decode("utf-8")
204+
self.assertNotIn("<script", decoded_logo)
205+
favicon_file.seek(0)
206+
207+
# Assert that the logo is completely sanitized
208+
with open(sanitized_svg, "rb") as sanitized_file:
209+
self.assertEqual(sanitized_file.read(), favicon_file.read())
210+
168211

169212
class GlobalConfirmationEmailTests(TestCase):
170213
def setUp(self):

src/openforms/config/tests/test_theme.py

+38
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .factories import ThemeFactory
1717

1818
LOGO_FILE = Path(settings.BASE_DIR) / "docs" / "logo.svg"
19+
PATH = Path(__file__).parent
1920

2021

2122
@disable_admin_mfa()
@@ -77,6 +78,43 @@ def test_upload_svg(self):
7778
style_tag.text(),
7879
)
7980

81+
def test_upload_malicious_svg(self):
82+
bad_svg = PATH / "data" / "bad_svg.svg"
83+
sanitized_svg = PATH / "data" / "sanitized_svg.svg"
84+
theme = ThemeFactory.create()
85+
86+
with self.subTest(part="admin config"):
87+
url = reverse("admin:config_theme_change", args=(theme.pk,))
88+
89+
change_page = self.app.get(url)
90+
91+
with open(bad_svg, "rb") as bad_file:
92+
# Assert that the bad svg contains script tags, which aren't allowed
93+
decoded_svg = bad_file.read().decode("utf-8")
94+
self.assertIn("<script", decoded_svg)
95+
bad_file.seek(0)
96+
97+
upload = Upload("bad_svg.svg", bad_file.read(), "image/svg+xml")
98+
99+
form = change_page.forms["theme_form"]
100+
form["logo"] = upload
101+
response = form.submit()
102+
103+
self.assertEqual(response.status_code, 302)
104+
theme.refresh_from_db()
105+
self.assertEqual(theme.logo, "logo/bad_svg.svg")
106+
107+
with self.subTest(part="assert logo sanitized"):
108+
with theme.logo.file.open("rb") as logo_file:
109+
# Assert that the logo has been sanitized and doesn't contain script tags
110+
decoded_logo = logo_file.read().decode("utf-8")
111+
self.assertNotIn("<script", decoded_logo)
112+
logo_file.seek(0)
113+
114+
# Assert that the logo is completely sanitized
115+
with open(sanitized_svg, "rb") as sanitized_file:
116+
self.assertEqual(sanitized_file.read(), logo_file.read())
117+
80118
def test_upload_png(self):
81119
logo = Path(settings.DJANGO_PROJECT_DIR) / "static" / "img" / "digid.png"
82120
theme = ThemeFactory.create()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from django.test import TestCase
2+
3+
from ..sanitizer import sanitize_svg_content
4+
5+
6+
class SanitizeSvgContentTests(TestCase):
7+
def test_sanitize_svg_content_removes_script_tags(self):
8+
bad_svg_content = (
9+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
10+
'<circle cx="25" cy="25" r="25" fill="green" />'
11+
"<script>//<![CDATA["
12+
'alert("I am malicious >:)")'
13+
"//]]></script>"
14+
"<g>"
15+
'<rect class="btn" x="0" y="0" width="10" height="10" fill="red" />'
16+
"</g>"
17+
"</svg>"
18+
)
19+
sanitized_svg_content = (
20+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
21+
'<circle cx="25" cy="25" r="25" fill="green"></circle>'
22+
"//"
23+
'alert("I am malicious &gt;:)")'
24+
"//"
25+
"<g>"
26+
'<rect x="0" y="0" width="10" height="10" fill="red"></rect>'
27+
"</g>"
28+
"</svg>"
29+
)
30+
31+
# Assert that sanitize_svg_content removed the script tag
32+
sanitized_bad_svg_content = sanitize_svg_content(bad_svg_content)
33+
self.assertEqual(sanitized_svg_content, sanitized_bad_svg_content)
34+
35+
36+
def test_sanitize_svg_content_removes_event_handlers(self):
37+
bad_svg_content = (
38+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">'
39+
'<circle cx="25" cy="25" r="25" fill="green" onclick="alert(\'forget about me?\')" />'
40+
"<g>"
41+
'<rect class="btn" x="0" y="0" width="10" height="10" fill="red" onload="alert(\'click!\')" />'
42+
"</g>"
43+
"</svg>"
44+
)
45+
sanitized_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"></circle>'
48+
"<g>"
49+
'<rect x="0" y="0" width="10" height="10" fill="red"></rect>'
50+
"</g>"
51+
"</svg>"
52+
)
53+
54+
# Assert that sanitize_svg_content removed the event handlers
55+
sanitized_bad_svg_content = sanitize_svg_content(bad_svg_content)
56+
self.assertEqual(sanitized_svg_content, sanitized_bad_svg_content)

0 commit comments

Comments
 (0)