From b1c406680f345f4aeec0989424dbcc839c75e438 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 16 Aug 2025 07:34:00 -0700 Subject: [PATCH] Merge commit from fork * Security: prevent XSS with storage path template rendering * Security: prevent XSS svg uploads * Security: force attachment disposition for logo * Add suggestions from code review * Improve SVG validation with allowlist for tags and attributes --- .../management-list.component.html | 2 + .../management-list.component.ts | 2 + .../storage-path-list.component.ts | 4 +- src/documents/tests/samples/malicious.svg | 4 + src/documents/tests/test_api_app_config.py | 31 ++++++ src/documents/views.py | 27 +++++ src/paperless/serialisers.py | 7 ++ src/paperless/urls.py | 10 +- src/paperless/validators.py | 102 ++++++++++++++++++ 9 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 src/documents/tests/samples/malicious.svg create mode 100644 src/paperless/validators.py diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.html b/src-ui/src/app/components/manage/management-list/management-list.component.html index 6375a3667..7e8f46511 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.html +++ b/src-ui/src/app/components/manage/management-list/management-list.component.html @@ -68,6 +68,8 @@ @if (column.rendersHtml) {
+ } @else if (column.monospace) { + {{ column.valueFn.call(null, object) }} } @else { {{ column.valueFn.call(null, object) }} } diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.ts b/src-ui/src/app/components/manage/management-list/management-list.component.ts index 670de2699..075a909a3 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.ts @@ -53,6 +53,8 @@ export interface ManagementListColumn { rendersHtml?: boolean hideOnMobile?: boolean + + monospace?: boolean } @Directive() diff --git a/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts b/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts index f14ba9aa3..5cab89bef 100644 --- a/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts +++ b/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts @@ -48,10 +48,10 @@ export class StoragePathListComponent extends ManagementListComponent { - return `${c.path?.slice(0, 49)}${c.path?.length > 50 ? '...' : ''}` + return `${c.path?.slice(0, 49)}${c.path?.length > 50 ? '...' : ''}` }, }, ] diff --git a/src/documents/tests/samples/malicious.svg b/src/documents/tests/samples/malicious.svg new file mode 100644 index 000000000..11fb65821 --- /dev/null +++ b/src/documents/tests/samples/malicious.svg @@ -0,0 +1,4 @@ + + Hello + + diff --git a/src/documents/tests/test_api_app_config.py b/src/documents/tests/test_api_app_config.py index 5968b1670..b43d312b7 100644 --- a/src/documents/tests/test_api_app_config.py +++ b/src/documents/tests/test_api_app_config.py @@ -149,6 +149,11 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): THEN: - old app_logo file is deleted """ + admin = User.objects.create_superuser(username="admin") + self.client.force_login(user=admin) + response = self.client.get("/logo/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + with (Path(__file__).parent / "samples" / "simple.jpg").open("rb") as f: self.client.patch( f"{self.ENDPOINT}1/", @@ -156,6 +161,12 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): "app_logo": f, }, ) + + # Logo exists at /logo/simple.jpg + response = self.client.get("/logo/simple.jpg") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("image/jpeg", response["Content-Type"]) + config = ApplicationConfiguration.objects.first() old_logo = config.app_logo self.assertTrue(Path(old_logo.path).exists()) @@ -168,6 +179,26 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): ) self.assertFalse(Path(old_logo.path).exists()) + def test_api_rejects_malicious_svg_logo(self): + """ + GIVEN: + - An SVG logo containing a