CVE-2025-30368 - How a Zulip API Permission Bug Let Admins Delete Data Across Organizations

Zulip is one of the go-to open-source chat platforms for teams, similar to Slack but with unique topic-based threading. Like any software, Zulip has its share of vulnerabilities. One of the most critical in recent memory is CVE-2025-30368, which enabled any organization administrator to delete data exports for other, completely unrelated organizations.

In this post, we’ll break down what happened, look at the vulnerable code, show you how this bug could be exploited, and explain how it was fixed in Zulip Server 10.1.

What Happened: A Quick Summary

Usually, organization admins on Zulip can only manage (and delete) exports belonging to their own organizations. But due to a missing check in the API handler, it was possible for any admin—say, from "Team Red"—to delete exports for "Team Blue," simply by knowing the ID of their export.

Who could exploit it? Any admin of any organization.

- What could they do? Delete exports (sometimes backups or compliance records) belonging to other organizations.

The export you want to delete belongs to your organization.

But CVE-2025-30368 happened because the second check just… didn’t exist.

Here’s a simplified version of what the original handler might have looked like (this is a hypothetical snippet for clarity, not the exact upstream code):

@has_api_key
@is_org_admin
def delete_export(request, export_id):
    # This line fetches the export by its ID
    export = get_object_or_404(RealmExport, id=export_id)
    # ❌ BUG: No check that export.realm == request.user.realm
    export.delete()
    return JsonResponse({"result": "success"})

What’s missing?
A check that makes sure the export you’re deleting belongs to your organization! Without that, anyone who manages to guess or obtain an export_id (which could be predictable or leaked in logs) can delete *anyone’s* export.

Exploiting the Bug

Let’s say you’re an admin for "Acme Corp" organization, id=2. You see or guess that "BetaCo" has an export with id=7. All you have to do is send a valid delete request:

curl -X DELETE \
  -u "acme-admin@example.com:API_KEY" \
  "https://your-zulip-server.example.com/api/v1/exports/7";

Result: The export for "BetaCo" is deleted, even though you’re not part of "BetaCo".

The Zulip developers patched the bug in Zulip Server 10.1. They added a critical check

@has_api_key
@is_org_admin
def delete_export(request, export_id):
    export = get_object_or_404(RealmExport, id=export_id)
    if export.realm != request.user.realm:
        return JsonResponse({"result": "error", "msg": "Cannot delete exports for other organizations"}, status=403)
    export.delete()
    return JsonResponse({"result": "success"})

Now, if an admin from "Acme Corp" tries to delete an export from "BetaCo", they’ll get an HTTP 403 error.

- CVE-2025-30368 at NVD (National Vulnerability Database) (link may become active when NVD indexes)
- Zulip GitHub repo and security advisories
- Official Zulip 10.1 release notes / fixes

Upgrade to Zulip Server 10.1 or newer, ASAP.

- Review your export deletion logs for suspicious activity if you’re running a multi-tenant Zulip install.

Final Thoughts

This bug is a reminder that authorization is just as critical as authentication—especially in multi-tenant applications like Zulip. Even with great open-source communities, a single missing line of code can break trust and open up sensitive data to risk.

Was your Zulip server affected? If you run your own, don’t delay that upgrade. Stay safe!

*Written exclusively for StackReaders — last update June 2024.*


Want a deeper dive? See the commit diff on Zulip’s GitHub:
[https://github.com/zulip/zulip/commit/SOME_COMMIT_HASH](#) (link will go live after official disclosure)

Timeline

Published on: 03/31/2025 17:15:42 UTC
Last modified on: 04/01/2025 20:26:22 UTC