CVE-2026-43967
Description
Inefficient Algorithmic Complexity vulnerability in absinthe-graphql absinthe allows unauthenticated denial of service via quadratic fragment-name uniqueness validation.
'Elixir.Absinthe.Phase.Document.Validation.UniqueFragmentNames':run/2 iterates over all fragments and for each one calls duplicate?/2, which evaluates Enum.count(fragments, &(&1.name == name)) — a full linear scan of the fragment list. The result is O(N²) comparisons per document, where N is the number of fragment definitions supplied by the caller.
Because input.fragments is built directly from the GraphQL query body, N is fully attacker-controlled. A minimum-size fragment definition is roughly 16 bytes, so a ~1 MB document carries ~60,000 fragments and forces ~3.6 × 10⁹ comparisons inside this single validation phase. No authentication, schema knowledge, or special configuration is required.
This issue affects absinthe: from 1.2.0 before 1.10.2.
Affected products
1Patches
1223600c52049Merge commit from fork
2 files changed · +153 −25
lib/absinthe/phase/document/validation/unique_fragment_names.ex+13 −25 modified@@ -12,32 +12,20 @@ defmodule Absinthe.Phase.Document.Validation.UniqueFragmentNames do """ @spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t() def run(input, _options \\ []) do - fragments = - for fragment <- input.fragments do - process(fragment, input.fragments) - end - - result = %{input | fragments: fragments} - {:ok, result} - end + counts = Enum.frequencies_by(input.fragments, & &1.name) - @spec process(Blueprint.Document.Fragment.Named.t(), [Blueprint.Document.Fragment.Named.t()]) :: - Blueprint.Document.Fragment.Named.t() - defp process(fragment, fragments) do - if duplicate?(fragments, fragment) do - fragment - |> flag_invalid(:duplicate_name) - |> put_error(error(fragment)) - else - fragment - end - end - - # Whether a duplicate fragment is present - @spec duplicate?([Blueprint.Document.Fragment.Named.t()], Blueprint.Document.Fragment.Named.t()) :: - boolean - defp duplicate?(fragments, fragment) do - Enum.count(fragments, &(&1.name == fragment.name)) > 1 + fragments = + Enum.map(input.fragments, fn fragment -> + if Map.fetch!(counts, fragment.name) > 1 do + fragment + |> flag_invalid(:duplicate_name) + |> put_error(error(fragment)) + else + fragment + end + end) + + {:ok, %{input | fragments: fragments}} end # Generate an error for a duplicate fragment.
test/absinthe/phase/document/validation/unique_fragment_names_test.exs+140 −0 added@@ -0,0 +1,140 @@ +defmodule Absinthe.Phase.Document.Validation.UniqueFragmentNamesTest do + @phase Absinthe.Phase.Document.Validation.UniqueFragmentNames + + use Absinthe.ValidationPhaseCase, + phase: @phase, + async: true + + alias Absinthe.{Blueprint, Phase, Pipeline} + + defp duplicate_fragment(name, line) do + bad_value( + Blueprint.Document.Fragment.Named, + @phase.error_message(name), + line, + name: name + ) + end + + describe "Validate: Unique fragment names" do + test "no fragments" do + assert_passes_validation( + """ + { + dog { name } + } + """, + [] + ) + end + + test "one fragment" do + assert_passes_validation( + """ + { + dog { ...dogFields } + } + fragment dogFields on Dog { + name + } + """, + [] + ) + end + + test "multiple unique fragments" do + assert_passes_validation( + """ + fragment fragA on Dog { + name + } + fragment fragB on Dog { + nickname + } + """, + [] + ) + end + + test "duplicate fragment names" do + assert_fails_validation( + """ + fragment fragA on Dog { + name + } + fragment fragA on Dog { + nickname + } + """, + [], + [ + duplicate_fragment("fragA", 1), + duplicate_fragment("fragA", 4) + ] + ) + end + + test "many duplicate fragment names" do + assert_fails_validation( + """ + fragment fragA on Dog { + name + } + fragment fragA on Dog { + nickname + } + fragment fragA on Dog { + barkVolume + } + """, + [], + [ + duplicate_fragment("fragA", 1), + duplicate_fragment("fragA", 4), + duplicate_fragment("fragA", 7) + ] + ) + end + + test "duplicate and unique fragments mixed" do + assert_fails_validation( + """ + fragment fragA on Dog { + name + } + fragment fragB on Dog { + nickname + } + fragment fragA on Dog { + barkVolume + } + """, + [], + [ + duplicate_fragment("fragA", 1), + duplicate_fragment("fragA", 7) + ] + ) + end + end + + # Regression test for CSV-TK + # UniqueFragmentNames.duplicate?/2 called Enum.count/2 (a full linear scan) + # for every fragment, producing O(N²) comparisons per document. An attacker + # can stall a worker for seconds with a single request containing many fragments. + describe "security: algorithmic complexity" do + test "validation of many unique fragments completes in linear time" do + n = 5_000 + + fragments = Enum.map_join(1..n, " ", fn i -> "fragment f#{i} on Dog { name }" end) + document = "{ dog { name } } " <> fragments + {:ok, blueprint, _} = Pipeline.run(document, [Phase.Parse, Phase.Blueprint]) + {elapsed_us, _result} = :timer.tc(fn -> @phase.run(blueprint, []) end) + elapsed_ms = div(elapsed_us, 1_000) + + assert elapsed_ms < 50, + "UniqueFragmentNames took #{elapsed_ms}ms for #{n} unique fragments — " <> + "expected < 50ms (linear). Quadratic behaviour detected." + end + end +end
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
6- github.com/advisories/GHSA-9mhv-8h52-q7q2ghsaADVISORY
- cna.erlef.org/cves/CVE-2026-43967.htmlnvd
- github.com/absinthe-graphql/absinthe/commit/223600c520493dcaf95080af552c413099f92c9dnvd
- github.com/absinthe-graphql/absinthe/security/advisories/GHSA-9mhv-8h52-q7q2nvd
- nvd.nist.gov/vuln/detail/CVE-2026-43967ghsa
- osv.dev/vulnerability/EEF-CVE-2026-43967nvd
News mentions
0No linked articles in our index yet.