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.
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.patchdefects/zulip-0002/patch/zulip-0002.patchdefects/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.
- Confirm receipt and assign a GitHub issue reference (zulip/zulip).
- Assess severity — zulip-0003 fires on every role/waiting-period change and scales with total realm membership.
- Coordinate a disclosure date — we target 90 days from first contact.
- 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.