diff --git a/src/documents/tests/test_api_app_config.py b/src/documents/tests/test_api_app_config.py index 974f0d205..6f487f5b0 100644 --- a/src/documents/tests/test_api_app_config.py +++ b/src/documents/tests/test_api_app_config.py @@ -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""" + + + + """ + + 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 + + + """ + + 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: diff --git a/src/paperless/validators.py b/src/paperless/validators.py index c1dd65298..bb741df41 100644 --- a/src/paperless/validators.py +++ b/src/paperless/validators.py @@ -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