Skip to content

Commit

Permalink
Allow disabling introspection (#1227, #456)
Browse files Browse the repository at this point in the history
- implement `validation::rules::disable_introspection`
- add `RootNode::disable_introspection()` and `RootNode::enable_introspection()` methods
  • Loading branch information
tyranron authored Nov 28, 2023
1 parent 58ae682 commit f98bdf1
Show file tree
Hide file tree
Showing 7 changed files with 480 additions and 4 deletions.
3 changes: 3 additions & 0 deletions juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
26 changes: 25 additions & 1 deletion juniper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down
60 changes: 59 additions & 1 deletion juniper/src/schema/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion juniper/src/validation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
mod context;
mod input_value;
mod multi_visitor;
mod rules;
pub mod rules;
mod traits;
mod visitor;

Expand Down
203 changes: 203 additions & 0 deletions juniper/src/validation/rules/disable_introspection.rs
Original file line number Diff line number Diff line change
@@ -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)],
)],
);
}
}
6 changes: 5 additions & 1 deletion juniper/src/validation/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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>)
Expand Down
Loading

0 comments on commit f98bdf1

Please sign in to comment.