java-topology/docs/tickets/phoenix-0001-channel-dispatch-event-intercepts-list-scan.md

2.6 KiB
Raw Permalink Blame History

phoenix-0001: CWE-407 — channel dispatch event in event_intercepts O(n) list scan per subscriber

Field Value
ID phoenix-0001
Project phoenixframework/phoenix
Severity HIGH
Status OPEN
CWE CWE-407 (Algorithmic Complexity — Inefficient Algorithmic Complexity)
Found 2026-03-27

Location

lib/phoenix/channel/server.ex:100

def dispatch(subscribers, from, %Broadcast{event: event} = msg) do
  Enum.reduce(subscribers, %{}, fn
    {pid, _}, cache when pid == from ->
      cache

    {pid, {:fastlane, fastlane_pid, serializer, event_intercepts}}, cache ->
      if event in event_intercepts do   # <-- CWE-407: O(n) list scan

Root Cause

event_intercepts is populated at channel join time from channel.__intercepts__(), which returns @phoenix_intercepts — a plain Elixir list accumulated at compile time:

# lib/phoenix/channel.ex:456,488,522-525
@phoenix_intercepts []
def __intercepts__, do: @phoenix_intercepts

defmacro intercept(events) do
  quote do: @phoenix_intercepts unquote(events)
end

The dispatch/3 function is called once per broadcast event and iterates every subscriber via Enum.reduce/3. For each fastlane subscriber it evaluates event in event_intercepts, which is List.member?/2 — O(k) where k is the number of intercepted events.

Total complexity per broadcast: O(subscribers × intercepts).

Impact

In a production Phoenix Channels deployment with N subscribers and K intercepted events, every broadcast/3 call costs O(N×K) membership tests. For a chat room with 10,000 subscribers and 5 intercepted events, this is 50,000 linear scans per broadcast message.

Channels are the highest-throughput path in Phoenix. Real-time applications (LiveView presence, multiplayer games, chat) broadcast frequently. This is a genuine hot-path defect.

Fix

Store event_intercepts as a MapSet at subscribe time:

# lib/phoenix/channel/server.ex:443 — change to MapSet
fastlane = {:fastlane, transport_pid, serializer, MapSet.new(channel.__intercepts__())}

# lib/phoenix/channel/server.ex:100 — already uses `in`, MapSet.member? is called automatically
if event in event_intercepts do   # MapSet.member? is O(1) hash lookup

The in operator in Elixir dispatches to Enumerable.member?/2, which for MapSet is O(1). No change to line 100 is needed — only the construction at line 443.

Patch

defects/phoenix/patch/phoenix-0001-channel-dispatch-event-intercepts-mapset.patch

Benchmark

defects/phoenix/unit/PhoenixTest.java