From f9884af1148ae4355230cfa121fb5d22081487b3 Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Tue, 21 Nov 2023 21:03:02 +0000 Subject: [PATCH] upload: Return images for 404/403 responses with image Accept: headers. If the request's `Accept:` header signals a preference for serving images over text, return an image representing the 404/403 instead of serving a `text/html` response. Fixes: #23739. --- static/images/errors/image-no-auth.png | Bin 0 -> 11067 bytes static/images/errors/image-not-exist.png | Bin 0 -> 11400 bytes zerver/tests/test_upload.py | 43 ++++++++++++++++++++++ zerver/views/upload.py | 44 ++++++++++++++++++++--- 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 static/images/errors/image-no-auth.png create mode 100644 static/images/errors/image-not-exist.png diff --git a/static/images/errors/image-no-auth.png b/static/images/errors/image-no-auth.png new file mode 100644 index 0000000000000000000000000000000000000000..316263403166cb076579d5f0f23cd29c7f6ca40f GIT binary patch literal 11067 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 literal 0 HcmV?d00001 diff --git a/zerver/tests/test_upload.py b/zerver/tests/test_upload.py index 9ed3310a01..3476d315b6 100644 --- a/zerver/tests/test_upload.py +++ b/zerver/tests/test_upload.py @@ -326,6 +326,26 @@ class FileUploadTest(UploadSerializeMixin, ZulipTestCase): self.assertEqual(response.status_code, 403) self.assert_in_response("

You are not authorized to view this file.

", response) + def test_image_download_unauthed(self) -> None: + """ + As the above, but with an Accept header that prefers images. + """ + self.login("hamlet") + fp = StringIO("zulip!") + fp.name = "zulip.txt" + result = self.client_post("/json/user_uploads", {"file": fp}) + response_dict = self.assert_json_success(result) + url = response_dict["uri"] + + self.logout() + response = self.client_get( + url, + # This is what Chrome sends for tags + headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"}, + ) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.headers["Content-Type"], "image/png") + def test_removed_file_download(self) -> None: """ Trying to download deleted files should return 404 error @@ -354,6 +374,29 @@ class FileUploadTest(UploadSerializeMixin, ZulipTestCase): self.assertEqual(response.status_code, 404) self.assert_in_response("This file does not exist or has been deleted.", response) + def test_non_existing_image_download(self) -> None: + """ + As the above method, but with an Accept header that prefers images to text + """ + hamlet = self.example_user("hamlet") + self.login_user(hamlet) + response = self.client_get( + f"http://{hamlet.realm.host}/user_uploads/{hamlet.realm_id}/ff/gg/abc.png", + # This is what Chrome sends for tags + headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"}, + ) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.headers["Content-Type"], "image/png") + + response = self.client_get( + f"http://{hamlet.realm.host}/user_uploads/{hamlet.realm_id}/ff/gg/abc.png", + # Ask for something neither image nor text -- you get text as a default + headers={"Accept": "audio/*,application/octet-stream"}, + ) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.headers["Content-Type"], "text/html; charset=utf-8") + self.assert_in_response("This file does not exist or has been deleted.", response) + def test_attachment_url_without_upload(self) -> None: hamlet = self.example_user("hamlet") self.login_user(hamlet) diff --git a/zerver/views/upload.py b/zerver/views/upload.py index 069c88e2e3..b7cd96a9ab 100644 --- a/zerver/views/upload.py +++ b/zerver/views/upload.py @@ -3,7 +3,7 @@ import binascii import os from datetime import timedelta from mimetypes import guess_type -from typing import Optional, Union +from typing import List, Optional, Union from urllib.parse import quote, urlparse from django.conf import settings @@ -20,13 +20,14 @@ from django.http import ( ) from django.shortcuts import redirect from django.urls import reverse -from django.utils.cache import patch_cache_control +from django.utils.cache import patch_cache_control, patch_vary_headers from django.utils.http import content_disposition_header from django.utils.translation import gettext as _ from zerver.context_processors import get_valid_realm_from_request from zerver.lib.exceptions import JsonableError from zerver.lib.response import json_success +from zerver.lib.storage import static_path from zerver.lib.upload import ( check_upload_within_quota, get_public_upload_root_url, @@ -167,6 +168,23 @@ def serve_file_url_backend( return serve_file(request, user_profile, realm_id_str, filename, url_only=True) +def preferred_accept(request: HttpRequest, served_types: List[str]) -> Optional[str]: + # Returns the first of the served_types which the browser will + # accept, based on the browser's stated quality preferences. + # Returns None if none of the served_types are accepted by the + # browser. + accepted_types = sorted( + request.accepted_types, + key=lambda e: float(e.params.get("q", "1.0")), + reverse=True, + ) + for potential_type in accepted_types: + for served_type in served_types: + if potential_type.match(served_type): + return served_type + return None + + def serve_file( request: HttpRequest, maybe_user_profile: Union[UserProfile, AnonymousUser], @@ -179,10 +197,28 @@ def serve_file( realm = get_valid_realm_from_request(request) is_authorized = validate_attachment_request(maybe_user_profile, path_id, realm) + def serve_image_error(status: int, image_path: str) -> HttpResponseBase: + # We cannot use X-Accel-Redirect to offload the serving of + # this image to nginx, because it does not preserve the status + # code of this response, nor the Vary: header. + return FileResponse(open(static_path(image_path), "rb"), status=status) # noqa: SIM115 + if is_authorized is None: - return HttpResponseNotFound(_("

This file does not exist or has been deleted.

")) + if preferred_accept(request, ["text/html", "image/png"]) == "image/png": + response = serve_image_error(404, "images/errors/image-not-exist.png") + else: + response = HttpResponseNotFound( + _("

This file does not exist or has been deleted.

") + ) + patch_vary_headers(response, ("Accept",)) + return response if not is_authorized: - return HttpResponseForbidden(_("

You are not authorized to view this file.

")) + if preferred_accept(request, ["text/html", "image/png"]) == "image/png": + response = serve_image_error(403, "images/errors/image-no-auth.png") + else: + response = HttpResponseForbidden(_("

You are not authorized to view this file.

")) + patch_vary_headers(response, ("Accept",)) + return response if url_only: url = generate_unauthed_file_access_url(path_id) return json_success(request, data=dict(url=url))