diff --git a/zerver/tests/test_upload.py b/zerver/tests/test_upload.py index ab4d01c0bb..768c3e453d 100644 --- a/zerver/tests/test_upload.py +++ b/zerver/tests/test_upload.py @@ -1182,6 +1182,18 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase): status_code=401, ) + with self.settings(RATE_LIMITING=True): + # Allow unauthenticated/spectator requests by ID for a reasonable number of requests. + add_ratelimit_rule(86400, 1000, domain="spectator_attachment_access_by_file") + response = self.client_get(f"/avatar/{cordelia.id}/medium", {"foo": "bar"}) + self.assertEqual(302, response.status_code) + remove_ratelimit_rule(86400, 1000, domain="spectator_attachment_access_by_file") + + # Deny file access since rate limited + add_ratelimit_rule(86400, 0, domain="spectator_attachment_access_by_file") + response = self.client_get(f"/avatar/{cordelia.id}/medium", {"foo": "bar"}) + self.assertEqual(429, response.status_code) + def test_non_valid_user_avatar(self) -> None: # It's debatable whether we should generate avatars for non-users, diff --git a/zerver/views/users.py b/zerver/views/users.py index 852a45c9d6..d98a6bff9e 100644 --- a/zerver/views/users.py +++ b/zerver/views/users.py @@ -40,10 +40,12 @@ from zerver.lib.exceptions import ( JsonableError, MissingAuthenticationError, OrganizationOwnerRequired, + RateLimited, ) from zerver.lib.integrations import EMBEDDED_BOTS +from zerver.lib.rate_limiter import rate_limit_spectator_attachment_access_by_file from zerver.lib.request import REQ, has_request_variables -from zerver.lib.response import json_success +from zerver.lib.response import json_response_from_error, json_success from zerver.lib.streams import access_stream_by_id, access_stream_by_name, subscribed_to_stream from zerver.lib.types import ProfileDataElementValue, Validator from zerver.lib.upload import upload_avatar_image @@ -251,6 +253,15 @@ def avatar( # interact with fake email addresses anyway. if is_email: raise MissingAuthenticationError() + + if settings.RATE_LIMITING: + try: + unique_avatar_key = f"{realm.id}/{email_or_id}/{medium}" + rate_limit_spectator_attachment_access_by_file(unique_avatar_key) + except RateLimited: + return json_response_from_error( + RateLimited(_("Too many attempts, please try after some time.")) + ) else: realm = maybe_user_profile.realm