2.1 KiB
phoenix-0002: CWE-407 — router scope pipe_through O(n²) duplicate detection via list scan
| Field | Value |
|---|---|
| ID | phoenix-0002 |
| Project | phoenixframework/phoenix |
| Severity | LOW (compile-time only) |
| Status | OPEN |
| CWE | CWE-407 (Algorithmic Complexity) |
| Found | 2026-03-27 |
Location
lib/phoenix/router/scope.ex:125
def pipe_through(module, new_pipes) do
new_pipes = List.wrap(new_pipes)
%{pipes: pipes} = top = get_top(module)
if pipe = Enum.find(new_pipes, &(&1 in pipes)) do # <-- O(n*m) list scan
raise ArgumentError, "duplicate pipe_through for #{inspect(pipe)}. ..."
end
put_top(module, %{top | pipes: pipes ++ new_pipes}) # <-- also O(n) append
end
Root Cause
The struct field pipes: [] (line 12) is a plain list. Enum.find(new_pipes, &(&1 in pipes))
iterates new_pipes (length m) and for each element does &1 in pipes — O(n) list membership
scan. Total: O(n×m).
Additionally pipes ++ new_pipes is O(n) list concatenation, which over repeated pipe_through
calls builds O(n²) total work.
Impact
Compile-time only. Router compilation runs once at startup (or code-reload). Routers with many pipelines accumulate O(P²) work where P is the total number of accumulated pipe names. For typical routers (P < 20) this is negligible in absolute time, but the pattern is wrong.
The pipes field should be a MapSet to make both the duplicate check and membership queries
O(1).
Fix
Change pipes: field from [] to MapSet.new() and update all usages:
# defstruct — change default
pipes: MapSet.new(),
# pipe_through — O(1) duplicate check
if pipe = Enum.find(new_pipes, &MapSet.member?(pipes, &1)) do
# accumulation — O(1) put instead of O(n) append
put_top(module, %{top | pipes: Enum.reduce(new_pipes, pipes, &MapSet.put(&2, &1))})
Callers of top.pipes that iterate over them (e.g. Enum.each) are unaffected — MapSet
implements Enumerable.
Patch
defects/phoenix/patch/phoenix-0002-router-scope-pipes-mapset.patch
Benchmark
defects/phoenix/unit/PhoenixTest.java