java-topology/whitepaper/outreach/zulip.md
russell@unturf.com 6784cdf1cf feat: add 39 outreach docs (batches 6-8)
Batch 6 (9): dolibarr, jitsi-videobridge, zed, tryton, suricata,
  strawberry, zulip, zesarux, zephyr
Batch 7 (15): xonotic (4), xash3d (3), xenia, xtuple, zabbix (2),
  zathura, zebra, yabause, zephyr-0001
Batch 8 (15): woodpecker (2), wine (4), widelands (3), wesnoth (3),
  wekan (3)

Mix of CWE-407 and CWE-312.
2026-04-14 17:06:28 -04:00

4.6 KiB

Zulip — CWE-407 Disclosure Brief

2026-04-13 · Patches available — awaiting upstream merge

Finding

Three O(n^2) defects in Zulip's user group subsystem across lock_subgroups_with_respect_to_supergroup, update_user_group, and update_users_in_full_members_system_group. All patched. Patches ready for upstream review. All three fire on user group membership operations via the API.

The Defects

zulip-0001 (PATCHED — MEDIUM): zerver/lib/user_groups.py:378

# list comprehension with `not in` list scan — O(G * F)
group_ids_found = [group.id for group in potential_subgroups]
group_ids_not_found = [
    group_id for group_id in potential_subgroup_ids if group_id not in group_ids_found
]

group_ids_found holds IDs from a queryset as a list. The not in membership test inside the list comprehension performs a linear scan per element. With G potential subgroup IDs and F found groups, total cost: O(G * F).

zulip-0002 (PATCHED — MEDIUM): zerver/lib/user_groups.py:476

# Identical pattern in update_user_group
group_ids_found = [group.id for group in potential_subgroups]
group_ids_not_found = [
    group_id for group_id in direct_subgroups if group_id not in group_ids_found
]

Same pattern in update_user_group. group_ids_found built as list, membership test O(F) per element. With D direct subgroups and F found groups, total cost: O(D * F).

zulip-0003 (PATCHED — MEDIUM): zerver/actions/user_groups.py:150

# list comprehension with `not in` list scan — O(M * F)
full_member_group_user_ids = [user["id"] for user in full_member_group_users]
members_excluding_full_members = [
    user for user in member_group_users if user["id"] not in full_member_group_user_ids
]

full_member_group_user_ids holds user IDs as a list. Each member triggers an O(F) linear scan. With M members and F full-members, total cost: O(M * F). Fires on every waiting-period threshold change and role change.

Complexity Proof

zulip-0001: At G=F=100 subgroups:

  • Defective: 100 * 100 = 10,000 comparisons
  • Fixed: 100 + 100 = 200 operations (set build + set lookups)
  • 50x speedup at G=100.

zulip-0002: At D=F=100:

  • Defective: 100 * 100 = 10,000 comparisons
  • Fixed: 200 operations
  • 50x speedup at D=100.

zulip-0003: At M=5,000 members, F=4,000 full-members:

  • Defective: 5,000 * 4,000 = 20,000,000 comparisons
  • Fixed: 4,000 + 5,000 = 9,000 operations
  • ~2,000x speedup at M=5,000.

Impact

Zulip serves as an open-source team chat platform used by thousands of organizations, including open-source communities, universities, and enterprises. Large Zulip realms (companies, open-source projects) can have thousands of members organized into complex group hierarchies.

zulip-0001 fires on every API request that adds or modifies user group membership hierarchies. zulip-0002 fires on every user group subgroup update. zulip-0003 fires on every waiting-period threshold change and user role change, affecting all members in the realm. On a Zulip realm with 5,000 members, zulip-0003 performs 20 million comparisons instead of 9,000.

The Fix

All three defects share the same fix pattern: convert list to set for O(1) membership testing.

zulip-0001:

# Before
group_ids_found = [group.id for group in potential_subgroups]

# After
group_ids_found_set = {group.id for group in potential_subgroups}

zulip-0002:

# Before
group_ids_found = [group.id for group in potential_subgroups]

# After
group_ids_found_set = {group.id for group in potential_subgroups}

zulip-0003:

# Before
full_member_group_user_ids = [user["id"] for user in full_member_group_users]

# After
full_member_group_user_ids_set = {user["id"] for user in full_member_group_users}

Patch

Fixes available:

  • defects/zulip-0001/patch/zulip-0001.patch
  • defects/zulip-0002/patch/zulip-0002.patch
  • defects/zulip-0003/patch/zulip-0003.patch

Three patches across zerver/lib/user_groups.py (two sites) and zerver/actions/user_groups.py (one site). All single-line list-to-set conversions. zulip-0001: 50x at G=100. zulip-0002: 50x at D=100. zulip-0003: ~2,000x at M=5,000.

What We Ask

Patches ready for review.

  1. Confirm receipt and assign a GitHub issue reference (zulip/zulip).
  2. Assess severity — zulip-0003 fires on every role/waiting-period change and scales with total realm membership.
  3. Coordinate a disclosure date — we target 90 days from first contact.
  4. We will credit the Zulip team in the public disclosure. Preferred acknowledgment format welcome.

Contact: see cover email. This brief is confidential until coordinated disclosure.