Skip to content

Commit f98bdf1

Browse files
authored
Allow disabling introspection (#1227, #456)
- implement `validation::rules::disable_introspection` - add `RootNode::disable_introspection()` and `RootNode::enable_introspection()` methods
1 parent 58ae682 commit f98bdf1

File tree

7 files changed

+480
-4
lines changed

7 files changed

+480
-4
lines changed

juniper/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
7070
- `LookAheadMethods::applies_for()` method. ([#1138], [#1145])
7171
- `LookAheadMethods::field_original_name()` and `LookAheadMethods::field_alias()` methods. ([#1199])
7272
- [`anyhow` crate] integration behind `anyhow` and `backtrace` [Cargo feature]s. ([#1215], [#988])
73+
- `RootNode::disable_introspection()` applying additional `validation::rules::disable_introspection`, and `RootNode::enable_introspection()` reverting it. ([#1227], [#456])
7374

7475
### Changed
7576

@@ -88,6 +89,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
8889
- Stack overflow on nested GraphQL fragments. ([CVE-2022-31173])
8990

9091
[#113]: /../../issues/113
92+
[#456]: /../../issues/456
9193
[#503]: /../../issues/503
9294
[#528]: /../../issues/528
9395
[#750]: /../../issues/750
@@ -140,6 +142,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
140142
[#1209]: /../../pull/1209
141143
[#1215]: /../../pull/1215
142144
[#1221]: /../../pull/1221
145+
[#1227]: /../../pull/1227
143146
[ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
144147
[CVE-2022-31173]: /../../security/advisories/GHSA-4rx6-g5vg-5f3j
145148

juniper/src/lib.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ use crate::{
5858
executor::{execute_validated_query, get_operation},
5959
introspection::{INTROSPECTION_QUERY, INTROSPECTION_QUERY_WITHOUT_DESCRIPTIONS},
6060
parser::parse_document_source,
61-
validation::{validate_input_values, visit_all_rules, ValidatorContext},
61+
validation::{
62+
rules, validate_input_values, visit as visit_rule, visit_all_rules, MultiVisitorNil,
63+
ValidatorContext,
64+
},
6265
};
6366

6467
pub use crate::{
@@ -158,6 +161,13 @@ where
158161
{
159162
let mut ctx = ValidatorContext::new(&root_node.schema, &document);
160163
visit_all_rules(&mut ctx, &document);
164+
if root_node.introspection_disabled {
165+
visit_rule(
166+
&mut MultiVisitorNil.with(rules::disable_introspection::factory()),
167+
&mut ctx,
168+
&document,
169+
);
170+
}
161171

162172
let errors = ctx.into_errors();
163173
if !errors.is_empty() {
@@ -201,6 +211,13 @@ where
201211
{
202212
let mut ctx = ValidatorContext::new(&root_node.schema, &document);
203213
visit_all_rules(&mut ctx, &document);
214+
if root_node.introspection_disabled {
215+
visit_rule(
216+
&mut MultiVisitorNil.with(rules::disable_introspection::factory()),
217+
&mut ctx,
218+
&document,
219+
);
220+
}
204221

205222
let errors = ctx.into_errors();
206223
if !errors.is_empty() {
@@ -246,6 +263,13 @@ where
246263
{
247264
let mut ctx = ValidatorContext::new(&root_node.schema, &document);
248265
visit_all_rules(&mut ctx, &document);
266+
if root_node.introspection_disabled {
267+
visit_rule(
268+
&mut MultiVisitorNil.with(rules::disable_introspection::factory()),
269+
&mut ctx,
270+
&document,
271+
);
272+
}
249273

250274
let errors = ctx.into_errors();
251275
if !errors.is_empty() {

juniper/src/schema/model.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ pub struct RootNode<
4444
pub subscription_info: SubscriptionT::TypeInfo,
4545
#[doc(hidden)]
4646
pub schema: SchemaType<'a, S>,
47+
#[doc(hidden)]
48+
pub introspection_disabled: bool,
4749
}
4850

4951
/// Metadata for a schema
@@ -147,7 +149,7 @@ where
147149
mutation_info: MutationT::TypeInfo,
148150
subscription_info: SubscriptionT::TypeInfo,
149151
) -> Self {
150-
RootNode {
152+
Self {
151153
query_type: query_obj,
152154
mutation_type: mutation_obj,
153155
subscription_type: subscription_obj,
@@ -159,9 +161,65 @@ where
159161
query_info,
160162
mutation_info,
161163
subscription_info,
164+
introspection_disabled: false,
162165
}
163166
}
164167

168+
/// Disables introspection for this [`RootNode`], making it to return a [`FieldError`] whenever
169+
/// its `__schema` or `__type` field is resolved.
170+
///
171+
/// By default, all introspection queries are allowed.
172+
///
173+
/// # Example
174+
///
175+
/// ```rust
176+
/// # use juniper::{
177+
/// # graphql_object, graphql_vars, EmptyMutation, EmptySubscription, GraphQLError,
178+
/// # RootNode,
179+
/// # };
180+
/// #
181+
/// pub struct Query;
182+
///
183+
/// #[graphql_object]
184+
/// impl Query {
185+
/// fn some() -> bool {
186+
/// true
187+
/// }
188+
/// }
189+
///
190+
/// type Schema = RootNode<'static, Query, EmptyMutation<()>, EmptySubscription<()>>;
191+
///
192+
/// let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new())
193+
/// .disable_introspection();
194+
///
195+
/// # // language=GraphQL
196+
/// let query = "query { __schema { queryType { name } } }";
197+
///
198+
/// match juniper::execute_sync(query, None, &schema, &graphql_vars! {}, &()) {
199+
/// Err(GraphQLError::ValidationError(errs)) => {
200+
/// assert_eq!(
201+
/// errs.first().unwrap().message(),
202+
/// "GraphQL introspection is not allowed, but the operation contained `__schema`",
203+
/// );
204+
/// }
205+
/// res => panic!("expected `ValidationError`, returned: {res:#?}"),
206+
/// }
207+
/// ```
208+
pub fn disable_introspection(mut self) -> Self {
209+
self.introspection_disabled = true;
210+
self
211+
}
212+
213+
/// Enables introspection for this [`RootNode`], if it was previously [disabled][1].
214+
///
215+
/// By default, all introspection queries are allowed.
216+
///
217+
/// [1]: RootNode::disable_introspection
218+
pub fn enable_introspection(mut self) -> Self {
219+
self.introspection_disabled = false;
220+
self
221+
}
222+
165223
#[cfg(feature = "schema-language")]
166224
/// The schema definition as a `String` in the
167225
/// [GraphQL Schema Language](https://graphql.org/learn/schema/#type-language)

juniper/src/validation/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
mod context;
44
mod input_value;
55
mod multi_visitor;
6-
mod rules;
6+
pub mod rules;
77
mod traits;
88
mod visitor;
99

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
//! Validation rule checking whether a GraphQL operation contains introspection (`__schema` or
2+
//! `__type` fields).
3+
4+
use crate::{
5+
ast::Field,
6+
parser::Spanning,
7+
validation::{ValidatorContext, Visitor},
8+
value::ScalarValue,
9+
};
10+
11+
/// Validation rule checking whether a GraphQL operation contains introspection (`__schema` or
12+
/// `__type` fields).
13+
pub struct DisableIntrospection;
14+
15+
/// Produces a new [`DisableIntrospection`] validation rule.
16+
#[inline]
17+
#[must_use]
18+
pub fn factory() -> DisableIntrospection {
19+
DisableIntrospection
20+
}
21+
22+
impl<'a, S> Visitor<'a, S> for DisableIntrospection
23+
where
24+
S: ScalarValue,
25+
{
26+
fn enter_field(
27+
&mut self,
28+
context: &mut ValidatorContext<'a, S>,
29+
field: &'a Spanning<Field<S>>,
30+
) {
31+
let field_name = field.item.name.item;
32+
if matches!(field_name, "__schema" | "__type") {
33+
context.report_error(&error_message(field_name), &[field.item.name.span.start]);
34+
}
35+
}
36+
}
37+
38+
fn error_message(field_name: &str) -> String {
39+
format!("GraphQL introspection is not allowed, but the operation contained `{field_name}`")
40+
}
41+
42+
#[cfg(test)]
43+
mod tests {
44+
use super::{error_message, factory};
45+
46+
use crate::{
47+
parser::SourcePosition,
48+
validation::{expect_fails_rule, expect_passes_rule, RuleError},
49+
value::DefaultScalarValue,
50+
};
51+
52+
#[test]
53+
fn allows_regular_fields() {
54+
// language=GraphQL
55+
expect_passes_rule::<_, _, DefaultScalarValue>(
56+
factory,
57+
r#"
58+
query {
59+
user {
60+
name
61+
... on User {
62+
email
63+
}
64+
alias: email
65+
... {
66+
typeless
67+
}
68+
friends {
69+
name
70+
}
71+
}
72+
}
73+
"#,
74+
);
75+
}
76+
77+
#[test]
78+
fn allows_typename_field() {
79+
// language=GraphQL
80+
expect_passes_rule::<_, _, DefaultScalarValue>(
81+
factory,
82+
r#"
83+
query {
84+
__typename
85+
user {
86+
__typename
87+
... on User {
88+
__typename
89+
}
90+
... {
91+
__typename
92+
}
93+
friends {
94+
__typename
95+
}
96+
}
97+
}
98+
"#,
99+
);
100+
}
101+
102+
#[test]
103+
fn forbids_query_schema() {
104+
// language=GraphQL
105+
expect_fails_rule::<_, _, DefaultScalarValue>(
106+
factory,
107+
r#"
108+
query {
109+
__schema {
110+
queryType {
111+
name
112+
}
113+
}
114+
}
115+
"#,
116+
&[RuleError::new(
117+
&error_message("__schema"),
118+
&[SourcePosition::new(37, 2, 16)],
119+
)],
120+
);
121+
}
122+
123+
#[test]
124+
fn forbids_query_type() {
125+
// language=GraphQL
126+
expect_fails_rule::<_, _, DefaultScalarValue>(
127+
factory,
128+
r#"
129+
query {
130+
__type(
131+
name: "Query"
132+
) {
133+
name
134+
}
135+
}
136+
"#,
137+
&[RuleError::new(
138+
&error_message("__type"),
139+
&[SourcePosition::new(37, 2, 16)],
140+
)],
141+
);
142+
}
143+
144+
#[test]
145+
fn forbids_field_type() {
146+
// language=GraphQL
147+
expect_fails_rule::<_, _, DefaultScalarValue>(
148+
factory,
149+
r#"
150+
query {
151+
user {
152+
name
153+
... on User {
154+
email
155+
}
156+
alias: email
157+
... {
158+
typeless
159+
}
160+
friends {
161+
name
162+
}
163+
__type
164+
}
165+
}
166+
"#,
167+
&[RuleError::new(
168+
&error_message("__type"),
169+
&[SourcePosition::new(370, 14, 20)],
170+
)],
171+
);
172+
}
173+
174+
#[test]
175+
fn forbids_field_schema() {
176+
// language=GraphQL
177+
expect_fails_rule::<_, _, DefaultScalarValue>(
178+
factory,
179+
r#"
180+
query {
181+
user {
182+
name
183+
... on User {
184+
email
185+
}
186+
alias: email
187+
... {
188+
typeless
189+
}
190+
friends {
191+
name
192+
}
193+
__schema
194+
}
195+
}
196+
"#,
197+
&[RuleError::new(
198+
&error_message("__schema"),
199+
&[SourcePosition::new(370, 14, 20)],
200+
)],
201+
);
202+
}
203+
}

juniper/src/validation/rules/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
//! Definitions of rules for validation.
2+
13
mod arguments_of_correct_type;
24
mod default_values_of_correct_type;
5+
pub mod disable_introspection;
36
mod fields_on_correct_type;
47
mod fragments_on_composite_types;
58
mod known_argument_names;
@@ -23,12 +26,13 @@ mod unique_variable_names;
2326
mod variables_are_input_types;
2427
mod variables_in_allowed_position;
2528

29+
use std::fmt::Debug;
30+
2631
use crate::{
2732
ast::Document,
2833
validation::{visit, MultiVisitorNil, ValidatorContext},
2934
value::ScalarValue,
3035
};
31-
use std::fmt::Debug;
3236

3337
#[doc(hidden)]
3438
pub fn visit_all_rules<'a, S: Debug>(ctx: &mut ValidatorContext<'a, S>, doc: &'a Document<S>)

0 commit comments

Comments
 (0)