Skip to content

Commit aa11681

Browse files
committed
add depth limit validator
1 parent fce45ef commit aa11681

File tree

6 files changed

+519
-0
lines changed

6 files changed

+519
-0
lines changed

docs/execution/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ Execution
1010
dataloader
1111
fileuploading
1212
subscriptions
13+
validators

docs/execution/validators.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Middleware
2+
==========
3+
4+
Validation rules help validate a given GraphQL query, before executing it.To help with common use
5+
cases, graphene provides a few validation rules out of the box.
6+
7+
8+
Depth limit Validator
9+
-----------------
10+
The depth limit validator helps to prevent execution of malicious
11+
queries. It takes in the following arguments.
12+
13+
- ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document.
14+
- ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean
15+
- ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation.
16+
17+
Example
18+
-------
19+
20+
Here is how you would implement depth-limiting on your schema.
21+
22+
.. code:: python
23+
from graphene.validators import depth_limit_validator
24+
25+
# The following schema doesn't execute queries
26+
# which have a depth more than 20.
27+
28+
result = schema.execute(
29+
'THE QUERY',
30+
validation_rules=[
31+
depth_limit_validator(
32+
max_depth=20
33+
)
34+
]
35+
)

graphene/validators/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .depth_limit_validator import depth_limit_validator
2+
3+
4+
__all__ = [
5+
"depth_limit_validator"
6+
]
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# This is a Python port of https://github.com/stems/graphql-depth-limit
2+
# which is licensed under the terms of the MIT license, reproduced below.
3+
#
4+
# -----------
5+
#
6+
# MIT License
7+
#
8+
# Copyright (c) 2017 Stem
9+
#
10+
# Permission is hereby granted, free of charge, to any person obtaining a copy
11+
# of this software and associated documentation files (the "Software"), to deal
12+
# in the Software without restriction, including without limitation the rights
13+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
# copies of the Software, and to permit persons to whom the Software is
15+
# furnished to do so, subject to the following conditions:
16+
#
17+
# The above copyright notice and this permission notice shall be included in all
18+
# copies or substantial portions of the Software.
19+
#
20+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26+
# SOFTWARE.
27+
28+
import re
29+
from typing import Callable, Dict, List, Optional, Union
30+
31+
from graphql import GraphQLError, is_introspection_type
32+
from graphql.language import (
33+
DefinitionNode,
34+
FieldNode,
35+
FragmentDefinitionNode,
36+
FragmentSpreadNode,
37+
InlineFragmentNode,
38+
Node,
39+
OperationDefinitionNode,
40+
)
41+
from graphql.validation import ValidationContext, ValidationRule
42+
43+
44+
IgnoreType = Union[Callable[[str], bool], re.Pattern, str]
45+
46+
47+
def depth_limit_validator(
48+
max_depth: int,
49+
ignore: Optional[List[IgnoreType]] = None,
50+
callback: Callable[[Dict[str, int]], None] = None,
51+
):
52+
class DepthLimitValidator(ValidationRule):
53+
def __init__(self, validation_context: ValidationContext):
54+
document = validation_context.document
55+
definitions = document.definitions
56+
57+
fragments = get_fragments(definitions)
58+
queries = get_queries_and_mutations(definitions)
59+
query_depths = {}
60+
61+
for name in queries:
62+
query_depths[name] = determine_depth(
63+
node=queries[name],
64+
fragments=fragments,
65+
depth_so_far=0,
66+
max_depth=max_depth,
67+
context=validation_context,
68+
operation_name=name,
69+
ignore=ignore,
70+
)
71+
72+
if callable(callback):
73+
callback(query_depths)
74+
super().__init__(validation_context)
75+
76+
return DepthLimitValidator
77+
78+
79+
def get_fragments(
80+
definitions: List[DefinitionNode],
81+
) -> Dict[str, FragmentDefinitionNode]:
82+
fragments = {}
83+
for definition in definitions:
84+
if isinstance(definition, FragmentDefinitionNode):
85+
fragments[definition.name.value] = definition
86+
87+
return fragments
88+
89+
90+
# This will actually get both queries and mutations.
91+
# We can basically treat those the same
92+
def get_queries_and_mutations(
93+
definitions: List[DefinitionNode],
94+
) -> Dict[str, OperationDefinitionNode]:
95+
operations = {}
96+
97+
for definition in definitions:
98+
if isinstance(definition, OperationDefinitionNode):
99+
operation = definition.name.value if definition.name else "anonymous"
100+
operations[operation] = definition
101+
102+
return operations
103+
104+
105+
def determine_depth(
106+
node: Node,
107+
fragments: Dict[str, FragmentDefinitionNode],
108+
depth_so_far: int,
109+
max_depth: int,
110+
context: ValidationContext,
111+
operation_name: str,
112+
ignore: Optional[List[IgnoreType]] = None,
113+
) -> int:
114+
if depth_so_far > max_depth:
115+
context.report_error(
116+
GraphQLError(
117+
f"'{operation_name}' exceeds maximum operation depth of {max_depth}",
118+
[node],
119+
)
120+
)
121+
return depth_so_far
122+
123+
if isinstance(node, FieldNode):
124+
# from: https://spec.graphql.org/June2018/#sec-Schema
125+
# > All types and directives defined within a schema must not have a name which
126+
# > begins with "__" (two underscores), as this is used exclusively
127+
# > by GraphQL’s introspection system.
128+
should_ignore = str(node.name.value).startswith("__") or is_ignored(
129+
node, ignore
130+
)
131+
132+
if should_ignore or not node.selection_set:
133+
return 0
134+
135+
return 1 + max(
136+
map(
137+
lambda selection: determine_depth(
138+
node=selection,
139+
fragments=fragments,
140+
depth_so_far=depth_so_far + 1,
141+
max_depth=max_depth,
142+
context=context,
143+
operation_name=operation_name,
144+
ignore=ignore,
145+
),
146+
node.selection_set.selections,
147+
)
148+
)
149+
elif isinstance(node, FragmentSpreadNode):
150+
return determine_depth(
151+
node=fragments[node.name.value],
152+
fragments=fragments,
153+
depth_so_far=depth_so_far,
154+
max_depth=max_depth,
155+
context=context,
156+
operation_name=operation_name,
157+
ignore=ignore,
158+
)
159+
elif isinstance(
160+
node, (InlineFragmentNode, FragmentDefinitionNode, OperationDefinitionNode)
161+
):
162+
return max(
163+
map(
164+
lambda selection: determine_depth(
165+
node=selection,
166+
fragments=fragments,
167+
depth_so_far=depth_so_far,
168+
max_depth=max_depth,
169+
context=context,
170+
operation_name=operation_name,
171+
ignore=ignore,
172+
),
173+
node.selection_set.selections,
174+
)
175+
)
176+
else:
177+
raise Exception(f"Depth crawler cannot handle: {node.kind}") # pragma: no cover
178+
179+
180+
def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bool:
181+
if ignore is None:
182+
return False
183+
184+
for rule in ignore:
185+
field_name = node.name.value
186+
if isinstance(rule, str):
187+
if field_name == rule:
188+
return True
189+
elif isinstance(rule, re.Pattern):
190+
if rule.match(field_name):
191+
return True
192+
elif callable(rule):
193+
if rule(field_name):
194+
return True
195+
else:
196+
raise ValueError(f"Invalid ignore option: {rule}")
197+
198+
return False

graphene/validators/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)