CVE-2025-30369 - Zulip Custom Profile Field Deletion Vulnerability (Explained with Code and Exploit Details)

CVE-2025-30369 is a newly reported vulnerability that affects Zulip, a popular open-source team chat platform. If you’re an IT admin or developer using Zulip for your organization, this is a critical read. Below, we detail the bug, show simple code snippets illustrating the issue, and explain how an attacker could have exploited this flaw before the fix released in Zulip Server 10.1.

What is Zulip?

Zulip is an open-source project that combines real-time chat with threaded conversations, making it a top choice for team collaboration. Zulip allows organizations to define custom profile fields, storing specific user information. Maintaining proper permissions around these fields is essential for data separation and security between different organizations (aka "realms") hosted on the same Zulip server.

What is CVE-2025-30369?

The vulnerability affects Zulip’s API endpoint for deleting custom profile fields. This API should only allow an administrator to delete profile fields belonging to their own organization. Due to insufficient validation, any admin from *any* organization could delete profile fields from other organizations as well.

Summary:
> The handler for deleting an organization's custom profile field did not check if the field actually belonged to the admin's organization. This allowed cross-organization deletion.

Let’s see a simplified example resembling Zulip's vulnerable code (before the fix)

# Vulnerable handler (sample)
def delete_custom_profile_field(request, field_id):
    user = request.user  # The admin making the request
    if not user.is_admin:
        return HttpResponseForbidden()
    try:
        field = CustomProfileField.objects.get(id=field_id)  # This fetches a global field, not filtered by org
    except CustomProfileField.DoesNotExist:
        return HttpResponseNotFound()
    field.delete()
    return HttpResponse(status=204)

The mistake:
The critical line above grabs the CustomProfileField by the id, not checking the organization that owns it. If you’re an admin in Organization A, you shouldn’t be able to delete a field in Organization B—but with this code, you can.

H "Authorization: Bearer ATTACKER_API_KEY" \

https://zulip.example.com/api/v1/realm/profile_fields/42

`

4. The profile field with ID 42, even if owned by Organization B, is deleted—because the server never checks organization ownership.

What Does the Patch Do?

The fix, available in Zulip Server 10.1, changes the handler to ensure the custom field belongs to the requester's organization:

# Fixed handler
def delete_custom_profile_field(request, field_id):
    user = request.user
    if not user.is_admin:
        return HttpResponseForbidden()
    try:
        # Only fetch the profile field in the admin's organization!
        field = CustomProfileField.objects.get(
            id=field_id, realm=user.realm
        )
    except CustomProfileField.DoesNotExist:
        return HttpResponseNotFound()
    field.delete()
    return HttpResponse(status=204)

Now, admins can only delete fields belonging to their own organization.

Multi-organization deployments are especially at risk.

- Prerequisite for an attacker: must be an admin of any organization on the Zulip server (not just a random user).


## References / Further Reading

- Official Zulip Security Advisory
- Upstream Zulip GitHub Pull Request (Patch)
- CVE Record on CVE.org
- Release Notes: Zulip 10.1

Conclusion

This cross-organization privilege escalation bug was subtle but dangerous. Thanks to the Zulip team for the quick response and patch! Always ensure your access checks include *all* necessary context, not just the resource ID.

If you run Zulip, double-check your server’s version and upgrade now.


*Post exclusive by AI Security Research, 2024. Please share responsibly.*

Timeline

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