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

Commit 32fa397

Browse files
fix: Add graphql max depth and aliases limits
1 parent 564cad4 commit 32fa397

File tree

4 files changed

+167
-7
lines changed

4 files changed

+167
-7
lines changed

codecov/settings_base.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@
9292

9393
WSGI_APPLICATION = "codecov.wsgi.application"
9494

95-
9695
# GraphQL
9796

9897
GRAPHQL_QUERY_COST_THRESHOLD = get_config(
@@ -105,6 +104,10 @@
105104

106105
GRAPHQL_RATE_LIMIT_RPM = get_config("setup", "graphql", "rate_limit_rpm", default=300)
107106

107+
GRAPHQL_MAX_DEPTH = 15
108+
109+
GRAPHQL_MAX_ALIASES = 15
110+
108111
# Database
109112
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
110113

@@ -184,7 +187,6 @@
184187

185188
USE_TZ = True
186189

187-
188190
# Static files (CSS, JavaScript, Images)
189191
# https://docs.djangoproject.com/en/2.1/howto/static-files/
190192

@@ -308,7 +310,6 @@
308310
"gitlab", "bots", "tokenless", "key", default=GITLAB_BOT_KEY
309311
)
310312

311-
312313
GITLAB_ENTERPRISE_CLIENT_ID = get_config("gitlab_enterprise", "client_id")
313314
GITLAB_ENTERPRISE_CLIENT_SECRET = get_config("gitlab_enterprise", "client_secret")
314315
GITLAB_ENTERPRISE_REDIRECT_URI = get_config(
@@ -323,7 +324,6 @@
323324
GITLAB_ENTERPRISE_URL = get_config("gitlab_enterprise", "url")
324325
GITLAB_ENTERPRISE_API_URL = get_config("gitlab_enterprise", "api_url")
325326

326-
327327
CORS_ALLOW_HEADERS = (
328328
list(default_headers)
329329
+ ["token-type"]
@@ -344,7 +344,6 @@
344344
"setup", "http", "file_upload_max_memory_size", default=2621440
345345
)
346346

347-
348347
CORS_ALLOWED_ORIGIN_REGEXES = get_config(
349348
"setup", "api_cors_allowed_origin_regexes", default=[]
350349
)
@@ -362,7 +361,6 @@
362361

363362
HIDE_ALL_CODECOV_TOKENS = get_config("setup", "hide_all_codecov_tokens", default=False)
364363

365-
366364
SENTRY_JWT_SHARED_SECRET = get_config(
367365
"sentry", "jwt_shared_secret", default=None
368366
) or get_config("setup", "sentry", "jwt_shared_secret", default=None)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
QueryType = GraphQLObjectType(
16+
"Query", {"field": GraphQLField(GraphQLString, resolve=lambda *args: "test")}
17+
)
18+
schema = GraphQLSchema(query=QueryType)
19+
20+
21+
def validate_query(query, *rules):
22+
ast = parse(query)
23+
return validate(schema, ast, rules=rules)
24+
25+
26+
# Tests for MaxDepthRule
27+
def test_max_depth_rule_allows_within_depth():
28+
query = """
29+
query {
30+
field
31+
}
32+
"""
33+
errors = validate_query(query, create_max_depth_rule(2))
34+
assert not errors, "Expected no errors for depth within the limit"
35+
36+
37+
def test_max_depth_rule_rejects_exceeding_depth():
38+
query = """
39+
query {
40+
field {
41+
field {
42+
field
43+
}
44+
}
45+
}
46+
"""
47+
errors = validate_query(query, create_max_depth_rule(2))
48+
assert errors, "Expected errors for exceeding depth limit"
49+
assert any(
50+
"Query depth exceeds the maximum allowed depth" in str(e) for e in errors
51+
)
52+
53+
54+
# Tests for MaxAliasesRule
55+
def test_max_aliases_rule_allows_within_alias_limit():
56+
query = """
57+
query {
58+
alias1: field
59+
alias2: field
60+
}
61+
"""
62+
errors = validate_query(query, create_max_aliases_rule(2))
63+
assert not errors, "Expected no errors for alias count within the limit"
64+
65+
66+
def test_max_aliases_rule_rejects_exceeding_alias_limit():
67+
query = """
68+
query {
69+
alias1: field
70+
alias2: field
71+
alias3: field
72+
}
73+
"""
74+
errors = validate_query(query, create_max_aliases_rule(2))
75+
assert errors, "Expected errors for exceeding alias limit"
76+
assert any("Query uses too many aliases" in str(e) for e in errors)
77+
78+
79+
def test_max_depth_rule_exact_depth():
80+
query = """
81+
query {
82+
field
83+
}
84+
"""
85+
errors = validate_query(query, create_max_depth_rule(2))
86+
assert not errors, "Expected no errors when query depth matches the limit"
87+
88+
89+
def test_max_aliases_rule_exact_alias_limit():
90+
query = """
91+
query {
92+
alias1: field
93+
alias2: field
94+
}
95+
"""
96+
errors = validate_query(query, create_max_aliases_rule(2))
97+
assert not errors, "Expected no errors when alias count matches the limit"

graphql_api/validation.py

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

graphql_api/views.py

Lines changed: 6 additions & 1 deletion
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

@@ -200,7 +201,11 @@ def get_validation_rules(
200201
maximum_cost=settings.GRAPHQL_QUERY_COST_THRESHOLD,
201202
default_cost=1,
202203
variables=data.get("variables"),
203-
)
204+
),
205+
create_max_depth_rule(max_depth=getattr(settings, "GRAPHQL_MAX_DEPTH", 15)),
206+
create_max_aliases_rule(
207+
max_aliases=getattr(settings, "GRAPHQL_MAX_ALIASES", 15)
208+
),
204209
]
205210

206211
validation_rules = get_validation_rules # type: ignore

0 commit comments

Comments
 (0)