Skip to content

Commit b9f39e8

Browse files
authored
Port MaxIntrospectionDepthRule from graphql-js (#904)
* https://github.com/graphql/graphql-js/blob/v17.0.0-alpha.7/src/validation/rules/MaxIntrospectionDepthRule.ts * https://github.com/graphql/graphql-js/blob/v17.0.0-alpha.7/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts Without this rule, a malicious client could cause huge introspection responses that grow exponentially with the list nesting level in the query. Instead of a validation rule, this check is performed as part of `SchemaIntrospectionSplit::split`. graphql-js allows users to disable or enable specific validation rules, with max introspection depth being part of "recommended" rules. apollo-compiler does not have this mechanism, so `document.validate()` always performs validation as per the GraphQL spec. The depth limit is hard-coded to 3, and applied to specific intropsection fields that matches graphql-js. graphql-js code has a comment about counting all list fields. When I tried that, a "normal" introspection query was rejected because `__schema.types[list].field[list].args[list]` reaches depth 3.
1 parent 6aa62a5 commit b9f39e8

File tree

7 files changed

+487
-8
lines changed

7 files changed

+487
-8
lines changed

crates/apollo-compiler/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2828
and matches their definition order in the schema.
2929
This helps fuzzing, where the same entropy source should generate the same test case.
3030

31+
## Features
32+
33+
- **Port MaxIntrospectionDepthRule from graphql-js - [SimonSapin] in [pull/904].**
34+
This limits list nesting in introspection query which can cause very large responses.
35+
3136
[SimonSapin]: https://github.com/SimonSapin
3237
[pull/898]: https://github.com/apollographql/apollo-rs/pull/898
38+
[pull/904]: https://github.com/apollographql/apollo-rs/pull/904
3339

3440

3541
# [1.0.0-beta.20](https://crates.io/crates/apollo-compiler/1.0.0-beta.20) - 2024-07-30

crates/apollo-compiler/src/execution/introspection_execute.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::execution::engine::ExecutionMode;
66
use crate::execution::resolver::ResolvedValue;
77
use crate::execution::JsonMap;
88
use crate::execution::Response;
9+
use crate::execution::SchemaIntrospectionError;
910
use crate::execution::SchemaIntrospectionSplit;
1011
use crate::schema;
1112
use crate::schema::Implementers;
@@ -23,7 +24,7 @@ use std::sync::OnceLock;
2324
///
2425
/// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
2526
#[derive(Clone, Debug)]
26-
pub struct SchemaIntrospectionQuery(pub(crate) Valid<ExecutableDocument>);
27+
pub struct SchemaIntrospectionQuery(Valid<ExecutableDocument>);
2728

2829
impl std::ops::Deref for SchemaIntrospectionQuery {
2930
type Target = Valid<ExecutableDocument>;
@@ -40,6 +41,22 @@ impl std::fmt::Display for SchemaIntrospectionQuery {
4041
}
4142

4243
impl SchemaIntrospectionQuery {
44+
/// Construct a `SchemaIntrospectionQuery` from a document
45+
/// assumed to contain exactly one operation that only has [schema introspection] fields.
46+
///
47+
/// Generally [`split`][SchemaIntrospectionSplit::split] should be used instead.
48+
///
49+
/// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
50+
#[doc(hidden)]
51+
// Hidden to discourage usage, but pub to allow Router "both" mode to use legacy code
52+
// for deciding what is or isn’t an introspection query.
53+
pub fn assume_only_intropsection_fields(
54+
document: Valid<ExecutableDocument>,
55+
) -> Result<Self, SchemaIntrospectionError> {
56+
super::introspection_max_depth::check_document(&document)?;
57+
Ok(Self(document))
58+
}
59+
4360
/// Execute the [schema introspection] parts of an operation
4461
/// and wrap a callback to execute the rest (if any).
4562
///
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use crate::executable::Selection;
2+
use crate::executable::SelectionSet;
3+
use crate::execution::introspection_split::get_fragment;
4+
use crate::execution::SchemaIntrospectionError;
5+
use crate::validation::Valid;
6+
use crate::ExecutableDocument;
7+
8+
const MAX_LISTS_DEPTH: u32 = 3;
9+
10+
pub(crate) fn check_document(
11+
document: &Valid<ExecutableDocument>,
12+
) -> Result<(), SchemaIntrospectionError> {
13+
for operation in document.operations.iter() {
14+
let initial_depth = 0;
15+
check_selection_set(document, initial_depth, &operation.selection_set)?;
16+
}
17+
Ok(())
18+
}
19+
20+
fn check_selection_set(
21+
document: &Valid<ExecutableDocument>,
22+
depth_so_far: u32,
23+
selection_set: &SelectionSet,
24+
) -> Result<(), SchemaIntrospectionError> {
25+
for selection in &selection_set.selections {
26+
match selection {
27+
Selection::InlineFragment(inline) => {
28+
check_selection_set(document, depth_so_far, &inline.selection_set)?
29+
}
30+
Selection::FragmentSpread(spread) => {
31+
// Validation ensures that `Valid<ExecutableDocument>` does not contain fragment cycles
32+
let def = get_fragment(document, &spread.fragment_name)?;
33+
check_selection_set(document, depth_so_far, &def.selection_set)?
34+
}
35+
Selection::Field(field) => {
36+
let mut depth = depth_so_far;
37+
if matches!(
38+
field.name.as_str(),
39+
"fields" | "interfaces" | "possibleTypes" | "inputFields"
40+
) {
41+
depth += 1;
42+
if depth >= MAX_LISTS_DEPTH {
43+
return Err(SchemaIntrospectionError::DeeplyNestedIntrospectionList(
44+
field.name.location(),
45+
));
46+
}
47+
}
48+
check_selection_set(document, depth, &field.selection_set)?
49+
}
50+
}
51+
}
52+
Ok(())
53+
}

crates/apollo-compiler/src/execution/introspection_split.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ pub enum SchemaIntrospectionSplit {
6565
#[derive(Debug)]
6666
pub enum SchemaIntrospectionError {
6767
SuspectedValidationBug(SuspectedValidationBug),
68+
DeeplyNestedIntrospectionList(Option<SourceSpan>),
6869
Unsupported {
6970
message: String,
7071
location: Option<SourceSpan>,
@@ -123,7 +124,9 @@ impl SchemaIntrospectionSplit {
123124
.collect();
124125
let introspection_document =
125126
make_single_operation_document(schema, document, new_operation, fragments);
126-
Ok(Self::Only(SchemaIntrospectionQuery(introspection_document)))
127+
Ok(Self::Only(
128+
SchemaIntrospectionQuery::assume_only_intropsection_fields(introspection_document)?,
129+
))
127130
} else {
128131
let mut fragments_done = HashSet::with_hasher(Default::default());
129132
let mut new_documents = Split {
@@ -136,11 +139,13 @@ impl SchemaIntrospectionSplit {
136139
&operation.selection_set,
137140
);
138141
Ok(Self::Both {
139-
introspection_query: SchemaIntrospectionQuery(new_documents.introspection.build(
140-
schema,
141-
document,
142-
operation_selection_set.introspection,
143-
)),
142+
introspection_query: SchemaIntrospectionQuery::assume_only_intropsection_fields(
143+
new_documents.introspection.build(
144+
schema,
145+
document,
146+
operation_selection_set.introspection,
147+
),
148+
)?,
144149
filtered_document: new_documents.other.build(
145150
schema,
146151
document,
@@ -244,7 +249,7 @@ fn make_single_operation_document(
244249
.expect("filtering a valid document should result in a valid document")
245250
}
246251

247-
fn get_fragment<'doc>(
252+
pub(crate) fn get_fragment<'doc>(
248253
document: &'doc Valid<ExecutableDocument>,
249254
name: &Name,
250255
) -> Result<&'doc Node<Fragment>, SchemaIntrospectionError> {
@@ -592,6 +597,9 @@ impl SchemaIntrospectionError {
592597
pub fn into_graphql_error(self, sources: &SourceMap) -> GraphQLError {
593598
match self {
594599
Self::SuspectedValidationBug(s) => s.into_graphql_error(sources),
600+
Self::DeeplyNestedIntrospectionList(location) => {
601+
GraphQLError::new("Maximum introspection depth exceeded", location, sources)
602+
}
595603
Self::Unsupported { message, location } => {
596604
GraphQLError::new(message, location, sources)
597605
}

crates/apollo-compiler/src/execution/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod resolver;
99
mod engine;
1010
mod input_coercion;
1111
mod introspection_execute;
12+
mod introspection_max_depth;
1213
mod introspection_split;
1314
mod response;
1415
mod result_coercion;

0 commit comments

Comments
 (0)