Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 63124e2

Browse files
fix: Add graphql max depth and aliases limits (#955)
1 parent 0273319 commit 63124e2

File tree

4 files changed

+174
-2
lines changed

4 files changed

+174
-2
lines changed

codecov/settings_base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@
106106

107107
GRAPHQL_INTROSPECTION_ENABLED = False
108108

109+
GRAPHQL_MAX_DEPTH = get_config("setup", "graphql", "max_depth", default=20)
110+
111+
GRAPHQL_MAX_ALIASES = get_config("setup", "graphql", "max_aliases", default=10)
112+
109113
# Database
110114
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
111115

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from graphql import (
2+
GraphQLField,
3+
GraphQLObjectType,
4+
GraphQLSchema,
5+
GraphQLString,
6+
parse,
7+
validate,
8+
)
9+
10+
from ..validation import (
11+
create_max_aliases_rule,
12+
create_max_depth_rule,
13+
)
14+
15+
16+
def resolve_field(*args):
17+
return "test"
18+
19+
20+
QueryType = GraphQLObjectType(
21+
"Query", {"field": GraphQLField(GraphQLString, resolve=resolve_field)}
22+
)
23+
schema = GraphQLSchema(query=QueryType)
24+
25+
26+
def validate_query(query, *rules):
27+
ast = parse(query)
28+
return validate(schema, ast, rules=rules)
29+
30+
31+
def test_max_depth_rule_allows_within_depth():
32+
query = """
33+
query {
34+
field
35+
}
36+
"""
37+
errors = validate_query(query, create_max_depth_rule(2))
38+
assert not errors, "Expected no errors for depth within the limit"
39+
40+
41+
def test_max_depth_rule_rejects_exceeding_depth():
42+
query = """
43+
query {
44+
field {
45+
field {
46+
field
47+
}
48+
}
49+
}
50+
"""
51+
errors = validate_query(query, create_max_depth_rule(2))
52+
assert errors, "Expected errors for exceeding depth limit"
53+
assert any(
54+
"Query depth exceeds the maximum allowed depth" in str(e) for e in errors
55+
)
56+
57+
58+
def test_max_depth_rule_exact_depth():
59+
query = """
60+
query {
61+
field
62+
}
63+
"""
64+
errors = validate_query(query, create_max_depth_rule(2))
65+
assert not errors, "Expected no errors when query depth matches the limit"
66+
67+
68+
def test_max_aliases_rule_allows_within_alias_limit():
69+
query = """
70+
query {
71+
alias1: field
72+
alias2: field
73+
}
74+
"""
75+
errors = validate_query(query, create_max_aliases_rule(2))
76+
assert not errors, "Expected no errors for alias count within the limit"
77+
78+
79+
def test_max_aliases_rule_rejects_exceeding_alias_limit():
80+
query = """
81+
query {
82+
alias1: field
83+
alias2: field
84+
alias3: field
85+
}
86+
"""
87+
errors = validate_query(query, create_max_aliases_rule(2))
88+
assert errors, "Expected errors for exceeding alias limit"
89+
assert any("Query uses too many aliases" in str(e) for e in errors)
90+
91+
92+
def test_max_aliases_rule_exact_alias_limit():
93+
query = """
94+
query {
95+
alias1: field
96+
alias2: field
97+
}
98+
"""
99+
errors = validate_query(query, create_max_aliases_rule(2))
100+
assert not errors, "Expected no errors when alias count matches the limit"

graphql_api/validation.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from typing import Any, Type
2+
3+
from graphql import GraphQLError, ValidationRule
4+
from graphql.language.ast import DocumentNode, FieldNode, OperationDefinitionNode
5+
from graphql.validation import ValidationContext
6+
7+
8+
def create_max_depth_rule(max_depth: int) -> Type[ValidationRule]:
9+
class MaxDepthRule(ValidationRule):
10+
def __init__(self, context: ValidationContext) -> None:
11+
super().__init__(context)
12+
self.operation_depth: int = 1
13+
self.max_depth_reached: bool = False
14+
self.max_depth: int = max_depth
15+
16+
def enter_operation_definition(
17+
self, node: OperationDefinitionNode, *_args: Any
18+
) -> None:
19+
self.operation_depth = 1
20+
self.max_depth_reached = False
21+
22+
def enter_field(self, node: FieldNode, *_args: Any) -> None:
23+
self.operation_depth += 1
24+
25+
if self.operation_depth > self.max_depth and not self.max_depth_reached:
26+
self.max_depth_reached = True
27+
self.report_error(
28+
GraphQLError(
29+
"Query depth exceeds the maximum allowed depth",
30+
node,
31+
)
32+
)
33+
34+
def leave_field(self, node: FieldNode, *_args: Any) -> None:
35+
self.operation_depth -= 1
36+
37+
return MaxDepthRule
38+
39+
40+
def create_max_aliases_rule(max_aliases: int) -> Type[ValidationRule]:
41+
class MaxAliasesRule(ValidationRule):
42+
def __init__(self, context: ValidationContext) -> None:
43+
super().__init__(context)
44+
self.alias_count: int = 0
45+
self.has_reported_error: bool = False
46+
self.max_aliases: int = max_aliases
47+
48+
def enter_document(self, node: DocumentNode, *_args: Any) -> None:
49+
self.alias_count = 0
50+
self.has_reported_error = False
51+
52+
def enter_field(self, node: FieldNode, *_args: Any) -> None:
53+
if node.alias:
54+
self.alias_count += 1
55+
56+
if self.alias_count > self.max_aliases and not self.has_reported_error:
57+
self.has_reported_error = True
58+
self.report_error(
59+
GraphQLError(
60+
"Query uses too many aliases",
61+
node,
62+
)
63+
)
64+
65+
return MaxAliasesRule

graphql_api/views.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from services.redis_configuration import get_redis_connection
2929

3030
from .schema import schema
31+
from .validation import create_max_aliases_rule, create_max_depth_rule
3132

3233
log = logging.getLogger(__name__)
3334

@@ -188,7 +189,7 @@ def __exit__(self, exc_type, exc_value, exc_traceback):
188189
class AsyncGraphqlView(GraphQLAsyncView):
189190
schema = schema
190191
extensions = [QueryMetricsExtension]
191-
introspection = getattr(settings, "GRAPHQL_INTROSPECTION_ENABLED", False)
192+
introspection = settings.GRAPHQL_INTROSPECTION_ENABLED
192193

193194
def get_validation_rules(
194195
self,
@@ -197,11 +198,13 @@ def get_validation_rules(
197198
data: dict,
198199
) -> Optional[Collection]:
199200
return [
201+
create_max_aliases_rule(max_aliases=settings.GRAPHQL_MAX_ALIASES),
202+
create_max_depth_rule(max_depth=settings.GRAPHQL_MAX_DEPTH),
200203
cost_validator(
201204
maximum_cost=settings.GRAPHQL_QUERY_COST_THRESHOLD,
202205
default_cost=1,
203206
variables=data.get("variables"),
204-
)
207+
),
205208
]
206209

207210
validation_rules = get_validation_rules # type: ignore

0 commit comments

Comments
 (0)