Add sanitation for SVG attachments (#3701)

* add svg parser

* move svg sanitation out into own file

* move allowed elements out

* add test for svg sanitation

* make allowed elements configureable
This commit is contained in:
Matthias Mair 2022-09-23 08:56:29 +02:00 committed by GitHub
parent db3d9b551a
commit 5a08ef908d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 95 additions and 0 deletions

View File

@ -4,6 +4,7 @@ import logging
import os import os
import re import re
from datetime import datetime from datetime import datetime
from io import BytesIO
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -24,6 +25,7 @@ import InvenTree.format
import InvenTree.helpers import InvenTree.helpers
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from InvenTree.fields import InvenTreeURLField from InvenTree.fields import InvenTreeURLField
from InvenTree.sanitizer import sanitize_svg
logger = logging.getLogger('inventree') logger = logging.getLogger('inventree')
@ -383,8 +385,16 @@ class InvenTreeAttachment(models.Model):
'link': _('Missing external link'), 'link': _('Missing external link'),
}) })
if self.attachment.name.lower().endswith('.svg'):
self.attachment.file.file = self.clean_svg(self.attachment)
super().save(*args, **kwargs) super().save(*args, **kwargs)
def clean_svg(self, field):
"""Sanitize SVG file before saving."""
cleaned = sanitize_svg(field.file.read())
return BytesIO(bytes(cleaned, 'utf8'))
def __str__(self): def __str__(self):
"""Human name for attachment.""" """Human name for attachment."""
if self.attachment is not None: if self.attachment is not None:

View File

@ -0,0 +1,67 @@
"""Functions to sanitize user input files."""
from bleach import clean
from bleach.css_sanitizer import CSSSanitizer
ALLOWED_ELEMENTS_SVG = [
'a', 'animate', 'animateColor', 'animateMotion',
'animateTransform', 'circle', 'defs', 'desc', 'ellipse', 'font-face',
'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern',
'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph',
'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect',
'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'
]
ALLOWED_ATTRIBUTES_SVG = [
'accent-height', 'accumulate', 'additive', 'alphabetic',
'arabic-form', 'ascent', 'attributeName', 'attributeType',
'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height',
'class', 'color', 'color-rendering', 'content', 'cx', 'cy', 'd', 'dx',
'dy', 'descent', 'display', 'dur', 'end', 'fill', 'fill-opacity',
'fill-rule', 'font-family', 'font-size', 'font-stretch', 'font-style',
'font-variant', 'font-weight', 'from', 'fx', 'fy', 'g1', 'g2',
'glyph-name', 'gradientUnits', 'hanging', 'height', 'horiz-adv-x',
'horiz-origin-x', 'id', 'ideographic', 'k', 'keyPoints',
'keySplines', 'keyTimes', 'lang', 'marker-end', 'marker-mid',
'marker-start', 'markerHeight', 'markerUnits', 'markerWidth',
'mathematical', 'max', 'min', 'name', 'offset', 'opacity', 'orient',
'origin', 'overline-position', 'overline-thickness', 'panose-1',
'path', 'pathLength', 'points', 'preserveAspectRatio', 'r', 'refX',
'refY', 'repeatCount', 'repeatDur', 'requiredExtensions',
'requiredFeatures', 'restart', 'rotate', 'rx', 'ry', 'slope',
'stemh', 'stemv', 'stop-color', 'stop-opacity',
'strikethrough-position', 'strikethrough-thickness', 'stroke',
'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap',
'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity',
'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to',
'transform', 'type', 'u1', 'u2', 'underline-position',
'underline-thickness', 'unicode', 'unicode-range', 'units-per-em',
'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x',
'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole',
'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title',
'xlink:type', 'xml:base', 'xml:lang', 'xml:space', 'xmlns',
'xmlns:xlink', 'y', 'y1', 'y2', 'zoomAndPan', 'style'
]
def sanitize_svg(file_data: str, strip: bool = True, elements: str = ALLOWED_ELEMENTS_SVG, attributes: str = ALLOWED_ATTRIBUTES_SVG) -> str:
"""Sanatize a SVG file.
Args:
file_data (str): SVG as string.
strip (bool, optional): Should invalid elements get removed. Defaults to True.
elements (str, optional): Allowed elements. Defaults to ALLOWED_ELEMENTS_SVG.
attributes (str, optional): Allowed attributes. Defaults to ALLOWED_ATTRIBUTES_SVG.
Returns:
str: Sanitzied SVG file.
"""
cleaned = clean(
file_data,
tags=elements,
attributes=attributes,
strip=strip,
strip_comments=strip,
css_sanitizer=CSSSanitizer()
)
return cleaned

View File

@ -23,6 +23,7 @@ import InvenTree.helpers
import InvenTree.tasks import InvenTree.tasks
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from common.settings import currency_codes from common.settings import currency_codes
from InvenTree.sanitizer import sanitize_svg
from part.models import Part, PartCategory from part.models import Part, PartCategory
from stock.models import StockItem, StockLocation from stock.models import StockItem, StockLocation
@ -878,3 +879,20 @@ class BarcodeMixinTest(helpers.InvenTreeTestCase):
for barcode, hash in hashing_tests.items(): for barcode, hash in hashing_tests.items():
self.assertEqual(InvenTree.helpers.hash_barcode(barcode), hash) self.assertEqual(InvenTree.helpers.hash_barcode(barcode), hash)
class SanitizerTest(TestCase):
"""Simple tests for sanitizer functions."""
def test_svg_sanitizer(self):
"""Test that SVGs are sanitized acordingly."""
valid_string = """<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" height="400" width="400">{0}
<path id="path1" d="m -151.78571,359.62883 v 112.76373 l 97.068507,-56.04253 V 303.14815 Z" style="fill:#ddbc91;"></path>
</svg>"""
dangerous_string = valid_string.format('<script>alert();</script>')
# Test that valid string
self.assertEqual(valid_string, sanitize_svg(valid_string))
# Test that invalid string is cleanded
self.assertNotEqual(dangerous_string, sanitize_svg(dangerous_string))