Skip to content

Commit d421984

Browse files
committed
Added svg sanitation to the SVGOrImageField class, using the bleach library. The sanitation eliminates script tags, event handlers and non-essential tags and attributes (like Animation stuff, data-*, href) The allowed tags and attributes are derived from the MDN documentation about SVG content.
1 parent 069f6d5 commit d421984

File tree

3 files changed

+203
-6
lines changed

3 files changed

+203
-6
lines changed

src/openforms/utils/fields.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ class StringUUIDField(models.UUIDField):
1414

1515

1616
class SVGOrImageField(models.ImageField):
17-
# SVG's are not regular "images", so they get different treatment. Note that
18-
# we're not doing extended sanitization *yet* here, so be careful when using
19-
# this field.
17+
# SVG's are not regular "images", so they get different treatment. Sanitization is
18+
# done in the form_class, form_fields.SVGOrImageField.
2019
def formfield(self, **kwargs):
2120
return super().formfield(
2221
**{

src/openforms/utils/form_fields.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from django.db import models
1010
from django.forms import CheckboxSelectMultiple, ImageField, MultipleChoiceField
1111

12+
from .sanitizer import sanitize_svg_file
13+
1214
image_or_svg_extension_validator = FileExtensionValidator(
1315
allowed_extensions=["svg"] + list(get_available_image_extensions())
1416
)
@@ -44,10 +46,10 @@ def to_python(self, data):
4446
# get the extension
4547
extension = get_extension(data)
4648

47-
# SVG's are not regular "images", so they get different treatment. Note that
48-
# we're not doing extended sanitization *yet* here, so be careful when using
49-
# this field.
49+
# SVG's are not regular "images", so they get different treatment.
5050
if extension == "svg":
51+
data = sanitize_svg_file(data)
52+
5153
# we call the parent of the original ImageField instead of calling to_python
5254
# of ImageField (the direct superclass), as that one tries to validate the
5355
# image with PIL

src/openforms/utils/sanitizer.py

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
from django.core.files import File
2+
3+
import bleach
4+
5+
ALLOWED_SVG_TAGS = (
6+
"circle",
7+
"clipPath",
8+
"defs",
9+
"desc",
10+
"ellipse",
11+
"feBlend",
12+
"feColorMatrix",
13+
"feComponentTransfer",
14+
"feComposite",
15+
"feConvolveMatrix",
16+
"feDiffuseLighting",
17+
"feDisplacementMap",
18+
"feDistantLight",
19+
"feDropShadow",
20+
"feFlood",
21+
"feFuncA",
22+
"feFuncB",
23+
"feFuncG",
24+
"feFuncR",
25+
"feGaussianBlur",
26+
"feImage",
27+
"feMerge",
28+
"feMergeNode",
29+
"feMorphology",
30+
"feOffset",
31+
"fePointLight",
32+
"feSpecularLighting",
33+
"feSpotLight",
34+
"feTile",
35+
"feTurbulence",
36+
"filter",
37+
"foreignObject",
38+
"g",
39+
"image",
40+
"line",
41+
"linearGradient",
42+
"marker",
43+
"mask",
44+
"metadata",
45+
"mpath",
46+
"path",
47+
"pattern",
48+
"polygon",
49+
"polyline",
50+
"radialGradient",
51+
"rect",
52+
"set",
53+
"stop",
54+
"style",
55+
"svg",
56+
"symbol",
57+
"text",
58+
"textPath",
59+
"title",
60+
"tspan",
61+
"use",
62+
"view",
63+
# --- Not allowing 'a', 'animate*' and 'script' tags
64+
)
65+
66+
ALLOWED_SVG_ATTRIBUTES = {
67+
"*": [
68+
# --- Basic presentation attributes
69+
"alignment-baseline",
70+
"baseline-shift",
71+
"clip",
72+
"clip-path",
73+
"clip-rule",
74+
"color",
75+
"color-interpolation",
76+
"color-interpolation-filters",
77+
"cursor",
78+
"cx",
79+
"cy",
80+
"d",
81+
"direction",
82+
"display",
83+
"dominant-baseline",
84+
"fill",
85+
"fill-opacity",
86+
"fill-rule",
87+
"filter",
88+
"flood-color",
89+
"flood-opacity",
90+
"font-family",
91+
"font-size",
92+
"font-size-adjust",
93+
"font-stretch",
94+
"font-style",
95+
"font-variant",
96+
"font-weight",
97+
"glyph-orientation-horizontal",
98+
"glyph-orientation-vertical",
99+
"height",
100+
"image-rendering",
101+
"letter-spacing",
102+
"lighting-color",
103+
"marker-end",
104+
"marker-mid",
105+
"marker-start",
106+
"mask",
107+
"mask-type",
108+
"opacity",
109+
"overflow",
110+
"pointer-events",
111+
"r",
112+
"rx",
113+
"ry",
114+
"shape-rendering",
115+
"stop-color",
116+
"stop-opacity",
117+
"stroke",
118+
"stroke-dasharray",
119+
"stroke-dashoffset",
120+
"stroke-linecap",
121+
"stroke-linejoin",
122+
"stroke-miterlimit",
123+
"stroke-opacity",
124+
"stroke-width",
125+
"text-anchor",
126+
"text-decoration",
127+
"text-overflow",
128+
"text-rendering",
129+
"transform",
130+
"transform-origin",
131+
"unicode-bidi",
132+
"vector-effect",
133+
"visibility",
134+
"white-space",
135+
"width",
136+
"word-spacing",
137+
"writing-mode",
138+
"x",
139+
"y",
140+
# --- Filter attributes
141+
"amplitude",
142+
"exponent",
143+
"intercept",
144+
"offset",
145+
"slope",
146+
"tableValues",
147+
"type",
148+
# --- Not allowing 'href', 'data-*', Animation and some other attributes
149+
],
150+
"svg": ["xmlns", "viewBox"],
151+
}
152+
153+
154+
def sanitize_svg_file(data: File) -> File:
155+
"""
156+
Defuse an uploaded SVG file.
157+
158+
The entire file content will be replaced with a sanitized version. All tags and
159+
attributes that aren't explicitly allowed, are removed from the SVG content.
160+
161+
:arg data: the uploaded SVG file, opened in binary mode.
162+
"""
163+
# Making sure that the file is reset properly
164+
data.seek(0)
165+
166+
file_content = data.read().decode("utf-8")
167+
sanitized_content = sanitize_svg_content(file_content)
168+
169+
# Replace svg file content with the bleached variant.
170+
# `truncate(0)` doesn't reset the point, so start with a seek(0) to make sure the
171+
# content is as expected.
172+
data.seek(0)
173+
data.truncate(0)
174+
data.write(sanitized_content.encode("utf-8"))
175+
176+
# Reset pointer
177+
data.seek(0)
178+
return data
179+
180+
181+
def sanitize_svg_content(svg_content: str) -> str:
182+
"""
183+
Strip (potentially) dangerous elements and attributes from the provided SVG string.
184+
185+
All tags and attributes that aren't explicitly allowed, are removed from the SVG
186+
content.
187+
188+
:arg svg_content: decoded SVG content.
189+
"""
190+
191+
return bleach.clean(
192+
svg_content,
193+
tags=ALLOWED_SVG_TAGS,
194+
attributes=ALLOWED_SVG_ATTRIBUTES,
195+
strip=True,
196+
)

0 commit comments

Comments
 (0)