]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Fix: allow safe <style> tags in SVG uploads (#11593) dev
authorshamoon <4887959+shamoon@users.noreply.github.com>
Fri, 12 Dec 2025 22:01:56 +0000 (14:01 -0800)
committerGitHub <noreply@github.com>
Fri, 12 Dec 2025 22:01:56 +0000 (22:01 +0000)
src/documents/tests/test_api_app_config.py
src/paperless/validators.py

index 974f0d205b3af6946431d2deca0ea2af0bce099c..6f487f5b0f90d570a0ba42b9a55305f12bc9b06d 100644 (file)
@@ -274,6 +274,35 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         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):
         """
         GIVEN:
@@ -326,6 +355,36 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
         )
         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):
         """
         GIVEN:
index c1dd652981193d099bc9e8af2c6f9d017ce00a54..bb741df416fc1b4709ae47533d19a48905e8950b 100644 (file)
@@ -17,6 +17,7 @@ ALLOWED_SVG_TAGS: set[str] = {
     "text",  # Text container
     "tspan",  # Text span within text
     "textpath",  # Text along a path
+    "style",  # Embedded CSS
     # Definitions and reusable content
     "defs",  # Container for reusable elements
     "symbol",  # Reusable graphic template
@@ -153,7 +154,9 @@ DANGEROUS_STYLE_PATTERNS: set[str] = {
     "@import",  # CSS @import directive
     "-moz-binding:",  # Firefox XBL bindings (can execute code)
     "behaviour:",  # IE behavior property
+    "behavior:",  # IE behavior property (US spelling)
     "vbscript:",  # VBScript URLs
+    "data:application/",  # Data URIs for arbitrary application payloads
 }
 
 XLINK_NS: set[str] = {
@@ -193,6 +196,15 @@ def reject_dangerous_svg(file: UploadedFile) -> None:
         if tag not in ALLOWED_SVG_TAGS:
             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_value: str
         for attr_name, attr_value in element.attrib.items():