Repository: graphql-rust/juniper (language: rust)

Issue:
Allow disabling introspection queries
**Is your feature request related to a problem? Please describe.**
I would like to avoid serving introspection in production builds of some services. I have a build pipeline that publishes the schema to an artifact at build time using `juniper::introspect`, so I really don't need the online schema to be available on my production services.

**Describe the solution you'd like**
I was looking for a way to disable introspection lookups on a RootNode, or filter introspection queries out at some layer of context, but I couldn't find an option to do that. 

**Describe alternatives you've considered**
Haven't found an alternative yet.

**Additional context**
I'm happy to implement this but I'm not sure where to hang this. On the executor? That way in src/schema/schema.rs resolve_field, I could selectively resolve or fail depending on whether introspection was enabled or disabled on the executor, but would that involve passing in a new option to .execute(). Or does it makes sense to configure the root node with this flag?


Discussion / hints:
It seems other graphql communities have something called "validations" that allow you to validate a query before even running it. Such as https://github.com/graphql-dotnet/graphql-dotnet/issues/766. That should probably be able to handle this. I wonder if it could also handle this https://github.com/graphql-rust/juniper/issues/447
>  I wonder if it could also handle this #447

Hmm... I guess not. The reason behind this that authorization is somewhat defined _on user-defined GraphQL types_, or even exists _before_ any schema resolution (so schema in this case is authorization-agnostic), while the introspection query does not interact with user-defined types, but their meta (which handled by Juniper automatically).

I guess this could be solved either allowing to attach some code for meta resolving or deeply inspecting the request before executing.
Any more thought was given to this? Introspection should be disabled in production environments unless you have a public-facing API where you need it, in order to avoid enumeration of types/queries.

It would be nice to opt queries and types out of the default public visibility.
Perhaps as a proc macro attribute where the default is to expose, but can be made private:

```rust
#[graphql(description = "..", visibility = "private")]
    fn query(context: &Context, ..) -> FieldResult<..> {
        //..
	}
```

But with this approach, there is the problem of: A change on type visibility should also affect related queries, or is the user responsibility to expose properly?

Or even the simplest option is to disable introspection server-wide while there is a better solution to individual visibility.
Is it even spec compliant to not allow introspection? We don't handle introspections in a special way, we just treat them as a normal query (and this is how they are handled in the spec).
While I am almost certain it is not mentioned in the spec, it is positively a good thing to have natively in the framework.  Please forgive me if there is already a mechanism to implement something like this, I still do not have a lot of familiarity with juniper.

Several frameworks allow disabling introspection nativaly, such as [Apollo Graphql](https://www.apollographql.com/docs/apollo-server/testing/graphql-playground/), [grapql-php](https://webonyx.github.io/graphql-php/security/#disabling-introspection), [graphql-ruby](https://github.com/rmosolgo/graphql-ruby/blob/master/guides/schema/introspection.md#disabling-introspection)

Other frameworks allow the user to replace the schema with another one through middleware, e.g [Python Graphene](https://medium.com/@pkinuthia10/disabling-djanog-graphene-introspection-query-8042b341c675)

Like @tyranron suggested, most of them inspect the query to check for `__schema` and `__type` introspection entry points.

We can find several security-oriented websites that recommend disabling introspection to make potential attackers have more trouble finding vulnerabilities, avoid leaking non-public types, and errors in general:

- [Owasp presentation - Jun 2020](https://owasp.org/www-chapter-vancouver/assets/presentations/2020-06_GraphQL_Security.pdf)
- [Introspection reveals non-public information is listed in the top 5 of GraphQL vulnerabilities](https://carvesystems.com/news/the-5-most-common-graphql-security-vulnerabilities/).


Thank you for this! I agree this is good to have.
Hi! I’m wondering if this could be done with an optional Cargo feature that disables introspection queries entirely. [Here’s a short version](https://github.com/graphql-rust/juniper/compare/a297b9b67e0b07e37ca52aa1b093b8ab4988c7c3...superkonduktr:juniper:disable-introspection) of how I did it in a project where introspection must be disabled due to security requirements. Would you be interested in adding something like this?

Thank you!
> It seems other graphql communities have something called "validations" that allow you to validate a query before even running it. Such as [graphql-dotnet/graphql-dotnet#766](https://github.com/graphql-dotnet/graphql-dotnet/issues/766).

The GraphQL over HTTP spec [mentions it](https://graphql.github.io/graphql-over-http/draft/#sel-FANJFABABlC15B) too.
> The server MAY, at its discretion, apply additional validation rules.

Fix patch:
diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md
index 90c01258e..6a228e6b5 100644
--- a/juniper/CHANGELOG.md
+++ b/juniper/CHANGELOG.md
@@ -70,6 +70,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 - `LookAheadMethods::applies_for()` method. ([#1138], [#1145])
 - `LookAheadMethods::field_original_name()` and `LookAheadMethods::field_alias()` methods. ([#1199])
 - [`anyhow` crate] integration behind `anyhow` and `backtrace` [Cargo feature]s. ([#1215], [#988])
+- `RootNode::disable_introspection()` applying additional `validation::rules::disable_introspection`, and `RootNode::enable_introspection()` reverting it. ([#1227], [#456])
 
 ### Changed
 
@@ -88,6 +89,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 - Stack overflow on nested GraphQL fragments. ([CVE-2022-31173])
 
 [#113]: /../../issues/113
+[#456]: /../../issues/456
 [#503]: /../../issues/503
 [#528]: /../../issues/528
 [#750]: /../../issues/750
@@ -140,6 +142,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 [#1209]: /../../pull/1209
 [#1215]: /../../pull/1215
 [#1221]: /../../pull/1221
+[#1227]: /../../pull/1227
 [ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
 [CVE-2022-31173]: /../../security/advisories/GHSA-4rx6-g5vg-5f3j
 
diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs
index 43c286482..88825a28d 100644
--- a/juniper/src/lib.rs
+++ b/juniper/src/lib.rs
@@ -58,7 +58,10 @@ use crate::{
     executor::{execute_validated_query, get_operation},
     introspection::{INTROSPECTION_QUERY, INTROSPECTION_QUERY_WITHOUT_DESCRIPTIONS},
     parser::parse_document_source,
-    validation::{validate_input_values, visit_all_rules, ValidatorContext},
+    validation::{
+        rules, validate_input_values, visit as visit_rule, visit_all_rules, MultiVisitorNil,
+        ValidatorContext,
+    },
 };
 
 pub use crate::{
@@ -158,6 +161,13 @@ where
     {
         let mut ctx = ValidatorContext::new(&root_node.schema, &document);
         visit_all_rules(&mut ctx, &document);
+        if root_node.introspection_disabled {
+            visit_rule(
+                &mut MultiVisitorNil.with(rules::disable_introspection::factory()),
+                &mut ctx,
+                &document,
+            );
+        }
 
         let errors = ctx.into_errors();
         if !errors.is_empty() {
@@ -201,6 +211,13 @@ where
     {
         let mut ctx = ValidatorContext::new(&root_node.schema, &document);
         visit_all_rules(&mut ctx, &document);
+        if root_node.introspection_disabled {
+            visit_rule(
+                &mut MultiVisitorNil.with(rules::disable_introspection::factory()),
+                &mut ctx,
+                &document,
+            );
+        }
 
         let errors = ctx.into_errors();
         if !errors.is_empty() {
@@ -246,6 +263,13 @@ where
     {
         let mut ctx = ValidatorContext::new(&root_node.schema, &document);
         visit_all_rules(&mut ctx, &document);
+        if root_node.introspection_disabled {
+            visit_rule(
+                &mut MultiVisitorNil.with(rules::disable_introspection::factory()),
+                &mut ctx,
+                &document,
+            );
+        }
 
         let errors = ctx.into_errors();
         if !errors.is_empty() {
diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs
index e7cba411d..81c376365 100644
--- a/juniper/src/schema/model.rs
+++ b/juniper/src/schema/model.rs
@@ -44,6 +44,8 @@ pub struct RootNode<
     pub subscription_info: SubscriptionT::TypeInfo,
     #[doc(hidden)]
     pub schema: SchemaType<'a, S>,
+    #[doc(hidden)]
+    pub introspection_disabled: bool,
 }
 
 /// Metadata for a schema
@@ -147,7 +149,7 @@ where
         mutation_info: MutationT::TypeInfo,
         subscription_info: SubscriptionT::TypeInfo,
     ) -> Self {
-        RootNode {
+        Self {
             query_type: query_obj,
             mutation_type: mutation_obj,
             subscription_type: subscription_obj,
@@ -159,9 +161,65 @@ where
             query_info,
             mutation_info,
             subscription_info,
+            introspection_disabled: false,
         }
     }
 
+    /// Disables introspection for this [`RootNode`], making it to return a [`FieldError`] whenever
+    /// its `__schema` or `__type` field is resolved.
+    ///
+    /// By default, all introspection queries are allowed.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// # use juniper::{
+    /// #     graphql_object, graphql_vars, EmptyMutation, EmptySubscription, GraphQLError,
+    /// #     RootNode,
+    /// # };
+    /// #
+    /// pub struct Query;
+    ///
+    /// #[graphql_object]
+    /// impl Query {
+    ///     fn some() -> bool {
+    ///         true
+    ///     }
+    /// }
+    ///
+    /// type Schema = RootNode<'static, Query, EmptyMutation<()>, EmptySubscription<()>>;
+    ///
+    /// let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new())
+    ///     .disable_introspection();
+    ///
+    /// # // language=GraphQL
+    /// let query = "query { __schema { queryType { name } } }";
+    ///
+    /// match juniper::execute_sync(query, None, &schema, &graphql_vars! {}, &()) {
+    ///     Err(GraphQLError::ValidationError(errs)) => {
+    ///         assert_eq!(
+    ///             errs.first().unwrap().message(),
+    ///             "GraphQL introspection is not allowed, but the operation contained `__schema`",
+    ///         );
+    ///     }
+    ///     res => panic!("expected `ValidationError`, returned: {res:#?}"),
+    /// }
+    /// ```
+    pub fn disable_introspection(mut self) -> Self {
+        self.introspection_disabled = true;
+        self
+    }
+
+    /// Enables introspection for this [`RootNode`], if it was previously [disabled][1].
+    ///
+    /// By default, all introspection queries are allowed.
+    ///
+    /// [1]: RootNode::disable_introspection
+    pub fn enable_introspection(mut self) -> Self {
+        self.introspection_disabled = false;
+        self
+    }
+
     #[cfg(feature = "schema-language")]
     /// The schema definition as a `String` in the
     /// [GraphQL Schema Language](https://graphql.org/learn/schema/#type-language)
diff --git a/juniper/src/validation/mod.rs b/juniper/src/validation/mod.rs
index c2759ec9f..d8d7a8177 100644
--- a/juniper/src/validation/mod.rs
+++ b/juniper/src/validation/mod.rs
@@ -3,7 +3,7 @@
 mod context;
 mod input_value;
 mod multi_visitor;
-mod rules;
+pub mod rules;
 mod traits;
 mod visitor;
 
diff --git a/juniper/src/validation/rules/disable_introspection.rs b/juniper/src/validation/rules/disable_introspection.rs
new file mode 100644
index 000000000..f9588d1e5
--- /dev/null
+++ b/juniper/src/validation/rules/disable_introspection.rs
@@ -0,0 +1,203 @@
+//! Validation rule checking whether a GraphQL operation contains introspection (`__schema` or
+//! `__type` fields).
+
+use crate::{
+    ast::Field,
+    parser::Spanning,
+    validation::{ValidatorContext, Visitor},
+    value::ScalarValue,
+};
+
+/// Validation rule checking whether a GraphQL operation contains introspection (`__schema` or
+/// `__type` fields).
+pub struct DisableIntrospection;
+
+/// Produces a new [`DisableIntrospection`] validation rule.
+#[inline]
+#[must_use]
+pub fn factory() -> DisableIntrospection {
+    DisableIntrospection
+}
+
+impl<'a, S> Visitor<'a, S> for DisableIntrospection
+where
+    S: ScalarValue,
+{
+    fn enter_field(
+        &mut self,
+        context: &mut ValidatorContext<'a, S>,
+        field: &'a Spanning<Field<S>>,
+    ) {
+        let field_name = field.item.name.item;
+        if matches!(field_name, "__schema" | "__type") {
+            context.report_error(&error_message(field_name), &[field.item.name.span.start]);
+        }
+    }
+}
+
+fn error_message(field_name: &str) -> String {
+    format!("GraphQL introspection is not allowed, but the operation contained `{field_name}`")
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{error_message, factory};
+
+    use crate::{
+        parser::SourcePosition,
+        validation::{expect_fails_rule, expect_passes_rule, RuleError},
+        value::DefaultScalarValue,
+    };
+
+    #[test]
+    fn allows_regular_fields() {
+        // language=GraphQL
+        expect_passes_rule::<_, _, DefaultScalarValue>(
+            factory,
+            r#"
+            query {
+                user {
+                    name
+                    ... on User {
+                        email
+                    }
+                    alias: email
+                    ... {
+                        typeless
+                    }
+                    friends {
+                        name
+                    }
+                }
+            }
+            "#,
+        );
+    }
+
+    #[test]
+    fn allows_typename_field() {
+        // language=GraphQL
+        expect_passes_rule::<_, _, DefaultScalarValue>(
+            factory,
+            r#"
+            query {
+                __typename
+                user {
+                    __typename
+                    ... on User {
+                        __typename
+                    }
+                    ... {
+                        __typename
+                    }
+                    friends {
+                        __typename
+                    }
+                }
+            }
+            "#,
+        );
+    }
+
+    #[test]
+    fn forbids_query_schema() {
+        // language=GraphQL
+        expect_fails_rule::<_, _, DefaultScalarValue>(
+            factory,
+            r#"
+            query {
+                __schema {
+                    queryType {
+                       name
+                    }
+                }
+            }
+            "#,
+            &[RuleError::new(
+                &error_message("__schema"),
+                &[SourcePosition::new(37, 2, 16)],
+            )],
+        );
+    }
+
+    #[test]
+    fn forbids_query_type() {
+        // language=GraphQL
+        expect_fails_rule::<_, _, DefaultScalarValue>(
+            factory,
+            r#"
+            query {
+                __type(
+                   name: "Query"
+                ) {
+                   name
+                }
+            }
+            "#,
+            &[RuleError::new(
+                &error_message("__type"),
+                &[SourcePosition::new(37, 2, 16)],
+            )],
+        );
+    }
+
+    #[test]
+    fn forbids_field_type() {
+        // language=GraphQL
+        expect_fails_rule::<_, _, DefaultScalarValue>(
+            factory,
+            r#"
+            query {
+                user {
+                    name
+                    ... on User {
+                        email
+                    }
+                    alias: email
+                    ... {
+                        typeless
+                    }
+                    friends {
+                        name
+                    }
+                    __type
+                }
+            }
+            "#,
+            &[RuleError::new(
+                &error_message("__type"),
+                &[SourcePosition::new(370, 14, 20)],
+            )],
+        );
+    }
+
+    #[test]
+    fn forbids_field_schema() {
+        // language=GraphQL
+        expect_fails_rule::<_, _, DefaultScalarValue>(
+            factory,
+            r#"
+            query {
+                user {
+                    name
+                    ... on User {
+                        email
+                    }
+                    alias: email
+                    ... {
+                        typeless
+                    }
+                    friends {
+                        name
+                    }
+                    __schema
+                }
+            }
+            "#,
+            &[RuleError::new(
+                &error_message("__schema"),
+                &[SourcePosition::new(370, 14, 20)],
+            )],
+        );
+    }
+}
diff --git a/juniper/src/validation/rules/mod.rs b/juniper/src/validation/rules/mod.rs
index 60a38d13d..6c22d2c92 100644
--- a/juniper/src/validation/rules/mod.rs
+++ b/juniper/src/validation/rules/mod.rs
@@ -1,5 +1,8 @@
+//! Definitions of rules for validation.
+
 mod arguments_of_correct_type;
 mod default_values_of_correct_type;
+pub mod disable_introspection;
 mod fields_on_correct_type;
 mod fragments_on_composite_types;
 mod known_argument_names;
@@ -23,12 +26,13 @@ mod unique_variable_names;
 mod variables_are_input_types;
 mod variables_in_allowed_position;
 
+use std::fmt::Debug;
+
 use crate::{
     ast::Document,
     validation::{visit, MultiVisitorNil, ValidatorContext},
     value::ScalarValue,
 };
-use std::fmt::Debug;
 
 #[doc(hidden)]
 pub fn visit_all_rules<'a, S: Debug>(ctx: &mut ValidatorContext<'a, S>, doc: &'a Document<S>)


Test patch:
diff --git a/tests/integration/tests/disabled_introspection.rs b/tests/integration/tests/disabled_introspection.rs
new file mode 100644
index 000000000..91e5c9dad
--- /dev/null
+++ b/tests/integration/tests/disabled_introspection.rs
@@ -0,0 +1,184 @@
+//! Checks whether [`RootNode::disable_introspection()`] works.
+
+use futures::stream;
+use juniper::{
+    execute, graphql_object, graphql_subscription, graphql_vars, resolve_into_stream, GraphQLError,
+    RootNode,
+};
+
+pub struct Query;
+
+#[graphql_object]
+impl Query {
+    fn some() -> bool {
+        true
+    }
+}
+
+pub struct Mutation;
+
+#[graphql_object]
+impl Mutation {
+    fn another() -> bool {
+        false
+    }
+}
+
+pub struct Subscription;
+
+#[graphql_subscription]
+impl Subscription {
+    async fn another() -> stream::Empty<bool> {
+        stream::empty()
+    }
+}
+
+#[tokio::test]
+async fn query_schema() {
+    // language=GraphQL
+    let query = "query { __schema { queryType { name } } }";
+
+    let schema = RootNode::new(Query, Mutation, Subscription).disable_introspection();
+
+    match execute(query, None, &schema, &graphql_vars! {}, &()).await {
+        Err(GraphQLError::ValidationError(errors)) => {
+            assert_eq!(errors.len(), 1);
+
+            let err = errors.first().unwrap();
+
+            assert_eq!(
+                err.message(),
+                "GraphQL introspection is not allowed, but the operation contained `__schema`",
+            );
+            assert_eq!(err.locations()[0].index(), 8);
+            assert_eq!(err.locations()[0].line(), 0);
+            assert_eq!(err.locations()[0].column(), 8);
+        }
+        res => panic!("expected `ValidationError`, returned: {res:#?}"),
+    }
+}
+
+#[tokio::test]
+async fn mutation_schema() {
+    // language=GraphQL
+    let query = "mutation { __schema { queryType { name } } }";
+
+    let schema = RootNode::new(Query, Mutation, Subscription).disable_introspection();
+
+    match execute(query, None, &schema, &graphql_vars! {}, &()).await {
+        Err(GraphQLError::ValidationError(errors)) => {
+            assert_eq!(errors.len(), 2);
+
+            let err = errors.first().unwrap();
+
+            assert_eq!(
+                err.message(),
+                "GraphQL introspection is not allowed, but the operation contained `__schema`",
+            );
+            assert_eq!(err.locations()[0].index(), 11);
+            assert_eq!(err.locations()[0].line(), 0);
+            assert_eq!(err.locations()[0].column(), 11);
+        }
+        res => panic!("expected `ValidationError`, returned: {res:#?}"),
+    }
+}
+
+#[tokio::test]
+async fn subscription_schema() {
+    // language=GraphQL
+    let query = "subscription { __schema { queryType { name } } }";
+
+    let schema = RootNode::new(Query, Mutation, Subscription).disable_introspection();
+
+    match resolve_into_stream(query, None, &schema, &graphql_vars! {}, &()).await {
+        Err(GraphQLError::ValidationError(errors)) => {
+            assert_eq!(errors.len(), 2);
+
+            let err = errors.first().unwrap();
+
+            assert_eq!(
+                err.message(),
+                "GraphQL introspection is not allowed, but the operation contained `__schema`",
+            );
+            assert_eq!(err.locations()[0].index(), 15);
+            assert_eq!(err.locations()[0].line(), 0);
+            assert_eq!(err.locations()[0].column(), 15);
+        }
+        _ => panic!("expected `ValidationError`"),
+    };
+}
+
+#[tokio::test]
+async fn query_type() {
+    // language=GraphQL
+    let query = r#"query { __type(name: "String") { name } }"#;
+
+    let schema = RootNode::new(Query, Mutation, Subscription).disable_introspection();
+
+    match execute(query, None, &schema, &graphql_vars! {}, &()).await {
+        Err(GraphQLError::ValidationError(errors)) => {
+            assert_eq!(errors.len(), 1);
+
+            let err = errors.first().unwrap();
+
+            assert_eq!(
+                err.message(),
+                "GraphQL introspection is not allowed, but the operation contained `__type`",
+            );
+            assert_eq!(err.locations()[0].index(), 8);
+            assert_eq!(err.locations()[0].line(), 0);
+            assert_eq!(err.locations()[0].column(), 8);
+        }
+        res => panic!("expected `ValidationError`, returned: {res:#?}"),
+    }
+}
+
+#[tokio::test]
+async fn mutation_type() {
+    // language=GraphQL
+    let query = r#"mutation { __type(name: "String") { name } }"#;
+
+    let schema = RootNode::new(Query, Mutation, Subscription).disable_introspection();
+
+    match execute(query, None, &schema, &graphql_vars! {}, &()).await {
+        Err(GraphQLError::ValidationError(errors)) => {
+            assert_eq!(errors.len(), 2);
+
+            let err = errors.first().unwrap();
+
+            assert_eq!(
+                err.message(),
+                "GraphQL introspection is not allowed, but the operation contained `__type`",
+            );
+            assert_eq!(err.locations()[0].index(), 11);
+            assert_eq!(err.locations()[0].line(), 0);
+            assert_eq!(err.locations()[0].column(), 11);
+        }
+        res => panic!("expected `ValidationError`, returned: {res:#?}"),
+    }
+}
+
+#[tokio::test]
+async fn subscription_type() {
+    // language=GraphQL
+    let query = r#"subscription Subscription { __type(name: "String") { name } }"#;
+
+    let schema = RootNode::new(Query, Mutation, Subscription).disable_introspection();
+
+    match resolve_into_stream(query, None, &schema, &graphql_vars! {}, &()).await {
+        Err(GraphQLError::ValidationError(errors)) => {
+            assert_eq!(errors.len(), 2);
+
+            let err = errors.first().unwrap();
+
+            assert_eq!(
+                err.message(),
+                "GraphQL introspection is not allowed, but the operation contained `__type`",
+            );
+            assert_eq!(err.locations()[0].index(), 28);
+            assert_eq!(err.locations()[0].line(), 0);
+            assert_eq!(err.locations()[0].column(), 28);
+        }
+        _ => panic!("expected `ValidationError`"),
+    };
+}