mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-18 01:41:14 -06:00
Fix: allow safe <style> tags in SVG uploads (#11593)
This commit is contained in:
@@ -274,6 +274,35 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertIn("disallowed", str(response.data).lower())
|
self.assertIn("disallowed", str(response.data).lower())
|
||||||
|
|
||||||
|
def test_api_rejects_svg_with_style_cdata_javascript(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- An SVG logo with javascript: hidden in a CDATA style block
|
||||||
|
WHEN:
|
||||||
|
- Uploaded via PATCH to app config
|
||||||
|
THEN:
|
||||||
|
- SVG is rejected with 400
|
||||||
|
"""
|
||||||
|
|
||||||
|
malicious_svg = b"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<style><![CDATA[
|
||||||
|
rect { background: url("javascript:alert('XSS')"); }
|
||||||
|
]]></style>
|
||||||
|
<rect width="100" height="100" fill="purple"/>
|
||||||
|
</svg>"""
|
||||||
|
|
||||||
|
svg_file = BytesIO(malicious_svg)
|
||||||
|
svg_file.name = "cdata_style.svg"
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
f"{self.ENDPOINT}1/",
|
||||||
|
{"app_logo": svg_file},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn("disallowed", str(response.data).lower())
|
||||||
|
|
||||||
def test_api_rejects_svg_with_style_import(self):
|
def test_api_rejects_svg_with_style_import(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@@ -326,6 +355,36 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_api_accepts_valid_svg_with_safe_style_tag(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A valid SVG logo with an embedded <style> tag
|
||||||
|
WHEN:
|
||||||
|
- Uploaded to app config
|
||||||
|
THEN:
|
||||||
|
- SVG is accepted with 200
|
||||||
|
"""
|
||||||
|
|
||||||
|
safe_svg = b"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<style>
|
||||||
|
rect { fill: #ff6b6b; stroke: #333; stroke-width: 2; }
|
||||||
|
circle { fill: white; opacity: 0.8; }
|
||||||
|
</style>
|
||||||
|
<rect width="100" height="100"/>
|
||||||
|
<circle cx="50" cy="50" r="30"/>
|
||||||
|
</svg>"""
|
||||||
|
|
||||||
|
svg_file = BytesIO(safe_svg)
|
||||||
|
svg_file.name = "safe_logo_with_style.svg"
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
f"{self.ENDPOINT}1/",
|
||||||
|
{"app_logo": svg_file},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
def test_api_rejects_svg_with_disallowed_attribute(self):
|
def test_api_rejects_svg_with_disallowed_attribute(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ ALLOWED_SVG_TAGS: set[str] = {
|
|||||||
"text", # Text container
|
"text", # Text container
|
||||||
"tspan", # Text span within text
|
"tspan", # Text span within text
|
||||||
"textpath", # Text along a path
|
"textpath", # Text along a path
|
||||||
|
"style", # Embedded CSS
|
||||||
# Definitions and reusable content
|
# Definitions and reusable content
|
||||||
"defs", # Container for reusable elements
|
"defs", # Container for reusable elements
|
||||||
"symbol", # Reusable graphic template
|
"symbol", # Reusable graphic template
|
||||||
@@ -153,7 +154,9 @@ DANGEROUS_STYLE_PATTERNS: set[str] = {
|
|||||||
"@import", # CSS @import directive
|
"@import", # CSS @import directive
|
||||||
"-moz-binding:", # Firefox XBL bindings (can execute code)
|
"-moz-binding:", # Firefox XBL bindings (can execute code)
|
||||||
"behaviour:", # IE behavior property
|
"behaviour:", # IE behavior property
|
||||||
|
"behavior:", # IE behavior property (US spelling)
|
||||||
"vbscript:", # VBScript URLs
|
"vbscript:", # VBScript URLs
|
||||||
|
"data:application/", # Data URIs for arbitrary application payloads
|
||||||
}
|
}
|
||||||
|
|
||||||
XLINK_NS: set[str] = {
|
XLINK_NS: set[str] = {
|
||||||
@@ -193,6 +196,15 @@ def reject_dangerous_svg(file: UploadedFile) -> None:
|
|||||||
if tag not in ALLOWED_SVG_TAGS:
|
if tag not in ALLOWED_SVG_TAGS:
|
||||||
raise ValidationError(f"Disallowed SVG tag: <{tag}>")
|
raise ValidationError(f"Disallowed SVG tag: <{tag}>")
|
||||||
|
|
||||||
|
if tag == "style":
|
||||||
|
# Combine all text (including CDATA) to scan for dangerous patterns
|
||||||
|
style_text: str = "".join(element.itertext()).lower()
|
||||||
|
for pattern in DANGEROUS_STYLE_PATTERNS:
|
||||||
|
if pattern in style_text:
|
||||||
|
raise ValidationError(
|
||||||
|
f"Disallowed pattern in <style> content: {pattern}",
|
||||||
|
)
|
||||||
|
|
||||||
attr_name: str
|
attr_name: str
|
||||||
attr_value: str
|
attr_value: str
|
||||||
for attr_name, attr_value in element.attrib.items():
|
for attr_name, attr_value in element.attrib.items():
|
||||||
|
|||||||
Reference in New Issue
Block a user