Skip to content
PR Quorum
Sign inStart free
← Review gallery
langgenius/dify #38997openreviewed 5h ago

fix: prevent hidden-tab collaboration leader from saving stale drafts

by Kevin9703+714 1010 filesreviewed in 1m 42sView PR on GitHub
minor notesThe panel surfaced minor issues worth a look before merge.4 findings

Reviewer panel

Correctness
deepseek/deepseek-v4-pro
1 found
Security
deepseek/deepseek-v4-pro
1 found
Architecture
deepseek/deepseek-v4-pro
2 found

Findings

01Mmedconf 90%

Unsafe conversion of client-provided graphActive value using bool()

api/services/workflow_collaboration_service.py:180

The server trusts the client-provided graphActive field in the graph_view_state event without proper validation. The value is converted with bool(), which treats non-empty strings (e.g., "false") as True, potentially allowing an attacker to falsely report a hidden tab as visible, thereby influencing leader election and causing stale drafts to be persisted.

Suggested fix

graph_active = event_data.get("graphActive") if isinstance(event_data.get("graphActive"), bool) else True

02Mmedconf 85%

sync_request re-route may cause leader churn when leader is hidden but no visible session exists

api/services/workflow_collaboration_service.py:200

When the leader is hidden and no visible session exists, the code falls back to leader_sid (the hidden leader) without re-electing. However, the condition replacement and replacement != leader_sid is checked after _select_graph_leader with preferred_sid=sid. If _select_graph_leader returns the requester (which is also hidden, since all tabs are hidden), replacement will equal sid, which is not leader_sid, so it will set the requester as the new leader. This is correct per the test. But if _select_graph_leader returns None (e.g., require_graph_active=True is not passed, but list_sessions returns empty due to a race), the code falls through to target_sid = leader_sid, keeping the hidden leader. This is intentional. The real issue: when _select_graph_leader returns the same hidden leader (because it's the only session), replacement == leader_sid is true, so target_sid stays leader_sid. That's fine. However, the else branch after if replacement and replacement != leader_sid sets target_sid = leader_sid without checking if replacement is None. If _select_graph_leader returns None (e.g., all sessions disconnected between the is_session_active check and list_sessions), target_sid becomes leader_sid, which may now be disconnected. The subsequent socketio.emit to that room will silently fail, and the sync request is lost. This is a pre-existing pattern in the codebase, but the new logic amplifies the window where _select_graph_leader can return None due to the two-tier selection. The test test_relay_collaboration_event_sync_request_all_hidden_falls_back_to_requester mocks list_sessions to return sessions, so it doesn't cover the None case.

03Llowconf 80%

Return value of update_session_graph_active is not checked

api/services/workflow_collaboration_service.py:184

The method update_session_graph_active returns False when the session is missing or the stored data is unreadable, but the caller in relay_collaboration_event ignores this return value. If the update fails (e.g. the session does not exist yet), the subsequent leader demotion logic may operate on stale graph_active state, potentially causing unnecessary leader churn. The failure is rare under normal operation, but defensive code should handle it.

04Llowconf 80%

update_session_graph_active read-modify-write is not atomic

api/repositories/workflow_collaboration_repository.py:183

The method reads the session JSON with hget, modifies it in Python, and writes it back with hset. If two concurrent requests (e.g., a graph_view_state event and a sync_request that triggers leader re-election) both call this method for the same session, one update can overwrite the other. This could cause a lost update on graph_active or other fields. In practice, the window is small and the consequence is a stale graph_active flag, which would self-correct on the next visibility change. However, in a collaboration system, this introduces a subtle race condition.

Want this on your own pull requests?
The same reviewer panel runs on every PR — one focused review, no noise.
Start freeLive demo