Coordinated Disclosure Timeline

Summary

Sentry version 25.11.0 contains a privilege escalation vulnerability where a scope mismatch issue could allow unauthorized users to delete events, potentially compromising the integrity of event data.

Project

Sentry

Tested Version

25.11.0

Details

Privilege escalation via scope mismatch allows deletion of events (GHSL-2025-120)

A privilege escalation vulnerability was identified in the group reprocessing flow of Sentry. Users with only event:write scope (not event:admin) can trigger mass deletion of issue (group) events by invoking the reprocessing endpoint with remainingEvents=”delete”. This bypasses the intended stricter permission for destructive deletion operations (which normally require event:admin for DELETE actions).

Vulnerability details

The vulnerability stems from the GroupPermission model only requiring event:write as the minimum permission for POST actions, while DELETE actions require event:admin:

A request to post on the GroupReprocessingEndpoint leads to checks whether the provided max_events number is at least 1 and whether remainingEvents is either keep or delete:

class GroupReprocessingEndpoint(GroupEndpoint):
[..]
    def post(self, request: Request, group) -> Response:
    [..]
        max_events = request.data.get("maxEvents")
        if max_events:
            max_events = int(max_events)

            if max_events <= 0:
                return self.respond({"error": "maxEvents must be at least 1"}, status=400)
        else:
            max_events = None

        remaining_events = request.data.get("remainingEvents")
        if remaining_events not in ("delete", "keep"):
            return self.respond({"error": "remainingEvents must be delete or keep"}, status=400)

After that the reprocess_group task is called which in turn calls buffered_handle_remaining_events, which then calls the handle_remaining_events task.

The handle_remaining_events task then deletes the events if remainingEvents was set to delete:

def handle_remaining_events
[..]
    if remaining_events == "delete":
        for cls in EVENT_MODELS_TO_MIGRATE:
            cls.objects.filter(project_id=project_id, event_id__in=event_ids).delete()

        # Remove from nodestore
        node_ids = [Event.generate_node_id(project_id, event_id) for event_id in event_ids]
        nodestore.backend.delete_multi(node_ids)

        # Tell Snuba to delete the event data.
        eventstream.backend.tombstone_events_unsafe(
            project_id, event_ids, from_timestamp=from_timestamp, to_timestamp=to_timestamp
        )

Impact

This issue may lead to privilege escalation and might allow unauthorized users to delete events.

CWEs

Credit

These issues were discovered with the GitHub Security Lab Taskflow Agent and verified by GHSL team members @m-y-mo (Man Yue Mo) and @p- (Peter Stöckli).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2025-120 in any communication regarding this issue.

Disclosure Policy

This report is subject to a 90-day disclosure deadline, as described in more detail in our coordinated disclosure policy.