diff --git a/src/documents/tests/test_api_app_config.py b/src/documents/tests/test_api_app_config.py index 214edbeee..63db4b766 100644 --- a/src/documents/tests/test_api_app_config.py +++ b/src/documents/tests/test_api_app_config.py @@ -12,6 +12,7 @@ from rest_framework import status from rest_framework.test import APITestCase from documents.tests.utils import DirectoriesMixin +from documents.tests.utils import read_streaming_response from paperless.models import ApplicationConfiguration from paperless.models import ColorConvertChoices @@ -193,6 +194,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): response = self.client.get("/logo/simple.jpg") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertIn("image/jpeg", response["Content-Type"]) + response.close() config = ApplicationConfiguration.objects.first() assert config is not None @@ -212,6 +214,46 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): ) self.assertFalse(Path(old_logo.path).exists()) + @override_settings(APP_LOGO="/logo/simple.jpg") + def test_serve_app_logo_from_environment_setting(self) -> None: + """ + GIVEN: + - No uploaded app logo + - PAPERLESS_APP_LOGO points to a file in the media logo directory + WHEN: + - The configured logo URL is requested + THEN: + - The environment-configured logo is served + """ + logo = self.dirs.media_dir / "logo" / "simple.jpg" + logo.parent.mkdir() + expected_content = ( + Path(__file__).parent / "samples" / "simple.jpg" + ).read_bytes() + logo.write_bytes(expected_content) + + response = self.client.get("/logo/simple.jpg") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("image/jpeg", response["Content-Type"]) + self.assertEqual(read_streaming_response(response), expected_content) + + @override_settings(APP_LOGO="/logo/../outside-logo.jpg") + def test_environment_app_logo_must_be_inside_logo_directory(self) -> None: + """ + GIVEN: + - PAPERLESS_APP_LOGO resolves outside the media logo directory + WHEN: + - The configured logo URL is requested + THEN: + - The file is not served + """ + (self.dirs.media_dir / "outside-logo.jpg").write_bytes(b"not a logo") + + response = self.client.get("/logo/outside-logo.jpg") + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_api_strips_exif_data_from_uploaded_logo(self) -> None: """ GIVEN: diff --git a/src/documents/views.py b/src/documents/views.py index a6bb6c955..a0e0d65dc 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -5348,15 +5348,26 @@ def serve_logo(request: HttpRequest, filename: str | None = None) -> FileRespons config = ApplicationConfiguration.objects.first() app_logo = config.app_logo - if not app_logo: - raise Http404("No logo configured") + if app_logo: + path = Path(app_logo.path) + logo_name = app_logo.name + else: + if not settings.APP_LOGO: + raise Http404("No logo configured") + + logo_root = (Path(settings.MEDIA_ROOT) / "logo").resolve() + path = (Path(settings.MEDIA_ROOT) / settings.APP_LOGO.lstrip("/")).resolve() + if not path.is_relative_to(logo_root) or not path.is_file(): + raise Http404("Configured logo not found") + + logo_name = path.name - path = app_logo.path content_type = magic.from_file(path, mime=True) or "application/octet-stream" + logo_file = app_logo.open("rb") if app_logo else path.open("rb") return FileResponse( - app_logo.open("rb"), + logo_file, content_type=content_type, - filename=app_logo.name, + filename=logo_name, as_attachment=True, )