/
githubmirror
/
zulip
Обзор
Документация
Войти
/
githubmirror
/
zulip
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
zerver/actions/uploads.py
143 строки
5 KB
Sahil Batra
CVE-2026-25742: Anonymous file access after disabling spectator access.
31 мар 2026, 21:25
31 мар 2026, 21:25
fd85f45
Код
Авторство
О чём код?
import logging from dataclasses import dataclass from typing import Any from django.db import transaction from zerver.lib.attachments import get_old_unclaimed_attachments, validate_attachment_request from zerver.lib.markdown import MessageRenderingResult from zerver.lib.upload import claim_attachment, delete_message_attachments from zerver.models import ( ArchivedAttachment, Attachment, ImageAttachment, Message, ScheduledMessage, Stream, UserProfile, ) from zerver.tornado.django_api import send_event_on_commit @dataclass class AttachmentChangeResult: did_attachment_change: bool detached_attachments: list[dict[str, Any]] def notify_attachment_update( user_profile: UserProfile, op: str, attachment_dict: dict[str, Any] ) -> None: event = { "type": "attachment", "op": op, "attachment": attachment_dict, "upload_space_used": user_profile.realm.currently_used_upload_space_bytes(), } send_event_on_commit(user_profile.realm, event, [user_profile.id]) def do_claim_attachments( message: Message | ScheduledMessage, potential_path_ids: list[str] ) -> bool: claimed = False for path_id in potential_path_ids: user_profile = message.sender is_message_realm_public = False is_message_web_public = False if isinstance(message, Message): is_channel_message = message.is_channel_message else: assert isinstance(message, ScheduledMessage) is_channel_message = message.is_channel_message() if is_channel_message: stream = Stream.objects.get(id=message.recipient.type_id) is_message_realm_public = stream.is_public() is_message_web_public = ( user_profile.realm.web_public_streams_enabled() and stream.is_web_public ) if not validate_attachment_request(user_profile, path_id)[0]: # Technically, there are 2 cases here: # * The user put something in their message that has the form # of an upload URL, but does not actually correspond to a previously # uploaded file. validate_attachment_request will return None. # * The user is trying to send a link to a file they don't have permission to # access themselves. validate_attachment_request will return False. # # Either case is unusual and suggests a UI bug that got # the user in this situation, so we log in these cases. logging.warning( "User %s tried to share upload %s in message %s, but lacks permission", user_profile.id, path_id, message.id, ) continue claimed = True attachment = claim_attachment( path_id, message, is_message_realm_public, is_message_web_public ) if not isinstance(message, ScheduledMessage): # attachment update events don't say anything about scheduled messages, # so sending an event is pointless. notify_attachment_update(user_profile, "update", attachment.to_dict()) return claimed @transaction.atomic(durable=True) def do_delete_old_unclaimed_attachments(weeks_ago: int) -> None: old_unclaimed_attachments, old_unclaimed_archived_attachments = get_old_unclaimed_attachments( weeks_ago ) with delete_message_attachments(delete_from=(ImageAttachment, Attachment)) as delete_one: for path_id in ( old_unclaimed_attachments.values_list("path_id", flat=True) .select_for_update(of=("self",), no_key=False) .iterator() ): delete_one(path_id) with delete_message_attachments( delete_from=(ImageAttachment, ArchivedAttachment) ) as delete_one: for path_id in ( old_unclaimed_archived_attachments.values_list("path_id", flat=True) .select_for_update(of=("self",), no_key=False) .iterator() ): delete_one(path_id) def check_attachment_reference_change( message: Message | ScheduledMessage, rendering_result: MessageRenderingResult ) -> AttachmentChangeResult: # For a unsaved message edit (message.* has been updated, but not # saved to the database), adjusts Attachment data to correspond to # the new content. prev_attachments = {a.path_id for a in message.attachment_set.all()} new_attachments = set(rendering_result.potential_attachment_path_ids) if new_attachments == prev_attachments: return AttachmentChangeResult(bool(prev_attachments), []) to_remove = list(prev_attachments - new_attachments) if len(to_remove) > 0: attachments_to_update = Attachment.objects.filter(path_id__in=to_remove).select_for_update( no_key=True ) message.attachment_set.remove(*attachments_to_update) sender = message.sender detached_attachments_query = Attachment.objects.filter( path_id__in=to_remove, messages__isnull=True, owner=sender ) detached_attachments = [attachment.to_dict() for attachment in detached_attachments_query] to_add = list(new_attachments - prev_attachments) if len(to_add) > 0: do_claim_attachments(message, to_add) return AttachmentChangeResult(message.attachment_set.exists(), detached_attachments)