From c4d66e948cfb2b64ce41eac4aac4ce534fb6cf5d Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:37:13 +1100 Subject: [PATCH 1/2] Backport #3526 with minor adjustments for v19 --- .../java/graphql/ErrorClassification.java | 17 +++ .../execution/AsyncExecutionStrategy.java | 8 ++ .../AsyncSerialExecutionStrategy.java | 9 ++ .../graphql/introspection/Introspection.java | 84 +++++++++++++ .../IntrospectionDisabledError.java | 35 ++++++ ...NoIntrospectionGraphqlFieldVisibility.java | 9 +- .../introspection/IntrospectionTest.groovy | 119 +++++++++++++++++- 7 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 src/main/java/graphql/introspection/IntrospectionDisabledError.java diff --git a/src/main/java/graphql/ErrorClassification.java b/src/main/java/graphql/ErrorClassification.java index e23f172810..db9764f2ce 100644 --- a/src/main/java/graphql/ErrorClassification.java +++ b/src/main/java/graphql/ErrorClassification.java @@ -23,4 +23,21 @@ public interface ErrorClassification { default Object toSpecification(GraphQLError error) { return String.valueOf(this); } + + /** + * This produces a simple ErrorClassification that represents the provided String. You can + * use this factory method to give out simple but custom error classifications. + * + * @param errorClassification the string that represents the error classification + * + * @return a ErrorClassification that is that provided string + */ + static ErrorClassification errorClassification(String errorClassification) { + return new ErrorClassification() { + @Override + public String toString() { + return errorClassification; + } + }; + } } diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index bdac02f5a9..9cad7903ed 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -5,10 +5,12 @@ import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; +import graphql.introspection.Introspection; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; @@ -46,6 +48,12 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); Set fieldNames = fields.keySet(); + + Optional isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(),fields); + if (isNotSensible.isPresent()) { + return CompletableFuture.completedFuture(isNotSensible.get()); + } + Async.CombinedBuilder futures = Async.ofExpectedSize(fields.size()); List resolvedFields = new ArrayList<>(fieldNames.size()); for (String fieldName : fieldNames) { diff --git a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java index bf094ac41b..7242578e89 100644 --- a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java @@ -6,8 +6,10 @@ import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; +import graphql.introspection.Introspection; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx; @@ -39,6 +41,13 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); ImmutableList fieldNames = ImmutableList.copyOf(fields.keySet()); + // this is highly unlikely since Mutations cant do introspection BUT in theory someone could make the query strategy this code + // so belts and braces + Optional isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(), fields); + if (isNotSensible.isPresent()) { + return CompletableFuture.completedFuture(isNotSensible.get()); + } + CompletableFuture> resultsFuture = Async.eachSequentially(fieldNames, (fieldName, index, prevResults) -> { MergedField currentField = fields.getSubField(fieldName); ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField)); diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index 3b8b5f40f0..3c088d5eaf 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -3,8 +3,13 @@ import com.google.common.collect.ImmutableSet; import graphql.Assert; +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.GraphQLContext; import graphql.Internal; import graphql.PublicApi; +import graphql.execution.MergedField; +import graphql.execution.MergedSelectionSet; import graphql.execution.ValuesResolver; import graphql.language.AstPrinter; import graphql.schema.FieldCoordinates; @@ -31,13 +36,16 @@ import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLUnionType; import graphql.schema.InputValueWithState; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import static graphql.Assert.assertTrue; @@ -56,8 +64,83 @@ import static graphql.schema.GraphQLTypeUtil.unwrapOne; import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; +/** + * GraphQl has a unique capability called Introspection that allow + * consumers to inspect the system and discover the fields and types available and makes the system self documented. + *

+ * Some security recommendations such as OWASP + * recommend that introspection be disabled in production. The {@link Introspection#enabledJvmWide(boolean)} method can be used to disable + * introspection for the whole JVM or you can place {@link Introspection#INTROSPECTION_DISABLED} into the {@link GraphQLContext} of a request + * to disable introspection for that request. + */ @PublicApi public class Introspection { + + + /** + * Placing a boolean value under this key in the per request {@link GraphQLContext} will enable + * or disable Introspection on that request. + */ + public static final String INTROSPECTION_DISABLED = "INTROSPECTION_DISABLED"; + private static final AtomicBoolean INTROSPECTION_ENABLED_STATE = new AtomicBoolean(true); + + /** + * This static method will enable / disable Introspection at a JVM wide level. + * + * @param enabled the flag indicating the desired enabled state + * + * @return the previous state of enablement + */ + public static boolean enabledJvmWide(boolean enabled) { + return INTROSPECTION_ENABLED_STATE.getAndSet(enabled); + } + + /** + * @return true if Introspection is enabled at a JVM wide level or false otherwise + */ + public static boolean isEnabledJvmWide() { + return INTROSPECTION_ENABLED_STATE.get(); + } + + /** + * This will look in to the field selection set and see if there are introspection fields, + * and if there is,it checks if introspection should run, and if not it will return an errored {@link ExecutionResult} + * that can be returned to the user. + * + * @param mergedSelectionSet the fields to be executed + * + * @return an optional error result + */ + public static Optional isIntrospectionSensible(GraphQLContext graphQLContext, MergedSelectionSet mergedSelectionSet) { + MergedField schemaField = mergedSelectionSet.getSubField(SchemaMetaFieldDef.getName()); + if (schemaField != null) { + if (!isIntrospectionEnabled(graphQLContext)) { + return mkDisabledError(schemaField); + } + } + MergedField typeField = mergedSelectionSet.getSubField(TypeMetaFieldDef.getName()); + if (typeField != null) { + if (!isIntrospectionEnabled(graphQLContext)) { + return mkDisabledError(typeField); + } + } + // later we can put a good faith check code here to check the fields make sense + return Optional.empty(); + } + + @NotNull + private static Optional mkDisabledError(MergedField schemaField) { + IntrospectionDisabledError error = new IntrospectionDisabledError(schemaField.getSingleField().getSourceLocation()); + return Optional.of(ExecutionResultImpl.newExecutionResult().addError(error).build()); + } + + private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) { + if (!isEnabledJvmWide()) { + return false; + } + return !graphQlContext.getOrDefault(INTROSPECTION_DISABLED, false); + } + private static final Map> introspectionDataFetchers = new LinkedHashMap<>(); private static void register(GraphQLFieldsContainer parentType, String fieldName, IntrospectionDataFetcher introspectionDataFetcher) { @@ -592,6 +675,7 @@ public enum DirectiveLocation { return environment.getGraphQLSchema().getType(name); }; + // __typename is always available public static final IntrospectionDataFetcher TypeNameMetaFieldDefDataFetcher = environment -> simplePrint(environment.getParentType()); @Internal diff --git a/src/main/java/graphql/introspection/IntrospectionDisabledError.java b/src/main/java/graphql/introspection/IntrospectionDisabledError.java new file mode 100644 index 0000000000..cbd7e077a3 --- /dev/null +++ b/src/main/java/graphql/introspection/IntrospectionDisabledError.java @@ -0,0 +1,35 @@ +package graphql.introspection; + +import graphql.ErrorClassification; +import graphql.ErrorType; +import graphql.GraphQLError; +import graphql.Internal; +import graphql.language.SourceLocation; + +import java.util.Collections; +import java.util.List; + +@Internal +public class IntrospectionDisabledError implements GraphQLError { + + private final List locations; + + public IntrospectionDisabledError(SourceLocation sourceLocation) { + locations = sourceLocation == null ? Collections.emptyList() : Collections.singletonList(sourceLocation); + } + + @Override + public String getMessage() { + return "Introspection has been disabled for this request"; + } + + @Override + public List getLocations() { + return locations; + } + + @Override + public ErrorClassification getErrorType() { + return ErrorClassification.errorClassification("IntrospectionDisabled"); + } +} diff --git a/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java b/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java index 2fc2fe6f00..475ecb014c 100644 --- a/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java +++ b/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java @@ -10,12 +10,17 @@ /** * This field visibility will prevent Introspection queries from being performed. Technically this puts your - * system in contravention of the specification - http://facebook.github.io/graphql/#sec-Introspection but some - * production systems want this lock down in place. + * system in contravention of the specification + * but some production systems want this lock down in place. + * + * @deprecated This is no longer the best way to prevent Introspection - {@link graphql.introspection.Introspection#enabledJvmWide(boolean)} + * can be used instead */ @PublicApi +@Deprecated // (since = "2024-03-16") public class NoIntrospectionGraphqlFieldVisibility implements GraphqlFieldVisibility { + @Deprecated // (since = "2024-03-16") public static NoIntrospectionGraphqlFieldVisibility NO_INTROSPECTION_FIELD_VISIBILITY = new NoIntrospectionGraphqlFieldVisibility(); diff --git a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy index a6633319b3..77ac0298d5 100644 --- a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy +++ b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy @@ -1,7 +1,8 @@ package graphql.introspection - +import graphql.ExecutionInput import graphql.TestUtil +import graphql.execution.AsyncSerialExecutionStrategy import graphql.schema.DataFetcher import graphql.schema.FieldCoordinates import graphql.schema.GraphQLCodeRegistry @@ -20,6 +21,14 @@ import static graphql.schema.GraphQLSchema.newSchema class IntrospectionTest extends Specification { + def setup() { + Introspection.enabledJvmWide(true) + } + + def cleanup() { + Introspection.enabledJvmWide(true) + } + def "bug 1186 - introspection depth check"() { def spec = ''' type Query { @@ -381,4 +390,112 @@ class IntrospectionTest extends Specification { arg['name'] == "arg" arg['defaultValue'] == "null" // printed AST } + + def "jvm wide enablement"() { + def graphQL = TestUtil.graphQL("type Query { f : String } ").build() + + when: + def er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors.isEmpty() + + when: + Introspection.enabledJvmWide(false) + er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors[0] instanceof IntrospectionDisabledError + er.errors[0].getErrorType().toString() == "IntrospectionDisabled" + + when: + Introspection.enabledJvmWide(true) + er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors.isEmpty() + } + + def "per request enablement"() { + def graphQL = TestUtil.graphQL("type Query { f : String } ").build() + + when: + // null context + def ei = ExecutionInput.newExecutionInput(IntrospectionQuery.INTROSPECTION_QUERY) + .build() + def er = graphQL.execute(ei) + + then: + er.errors.isEmpty() + + when: + ei = ExecutionInput.newExecutionInput(IntrospectionQuery.INTROSPECTION_QUERY) + .graphQLContext(["INTROSPECTION_DISABLED": false]).build() + er = graphQL.execute(ei) + + then: + er.errors.isEmpty() + + when: + ei = ExecutionInput.newExecutionInput(IntrospectionQuery.INTROSPECTION_QUERY) + .graphQLContext(["INTROSPECTION_DISABLED": true]).build() + er = graphQL.execute(ei) + + then: + er.errors[0] instanceof IntrospectionDisabledError + er.errors[0].getErrorType().toString() == "IntrospectionDisabled" + } + + def "mixed schema and other fields stop early"() { + def graphQL = TestUtil.graphQL("type Query { normalField : String } ").build() + + def query = """ + query goodAndBad { + normalField + __schema{ types{ fields { name }}} + } + """ + + when: + def er = graphQL.execute(query) + + then: + er.errors.isEmpty() + + when: + Introspection.enabledJvmWide(false) + er = graphQL.execute(query) + + then: + er.errors[0] instanceof IntrospectionDisabledError + er.errors[0].getErrorType().toString() == "IntrospectionDisabled" + er.data == null // stops hard + } + + def "AsyncSerialExecutionStrategy with jvm wide enablement"() { + def graphQL = TestUtil.graphQL("type Query { f : String } ") + .queryExecutionStrategy(new AsyncSerialExecutionStrategy()).build() + + when: + def er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors.isEmpty() + + when: + Introspection.enabledJvmWide(false) + er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors[0] instanceof IntrospectionDisabledError + er.errors[0].getErrorType().toString() == "IntrospectionDisabled" + + when: + Introspection.enabledJvmWide(true) + er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors.isEmpty() + } + } From dc3fe407e12143789ba0ec96647ace74232fdfc8 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Tue, 19 Mar 2024 09:56:41 +1100 Subject: [PATCH 2/2] Backport PR 3527 GoodFaithIntrospection --- .../execution/AsyncExecutionStrategy.java | 2 +- .../AsyncSerialExecutionStrategy.java | 2 +- .../introspection/GoodFaithIntrospection.java | 125 ++++++++++++++++ .../graphql/introspection/Introspection.java | 10 +- ...ithIntrospectionInstrumentationTest.groovy | 136 ++++++++++++++++++ ...ormalizedOperationToAstCompilerTest.groovy | 48 ++++--- 6 files changed, 302 insertions(+), 21 deletions(-) create mode 100644 src/main/java/graphql/introspection/GoodFaithIntrospection.java create mode 100644 src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 9cad7903ed..6bfbc2bb0a 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -49,7 +49,7 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); Set fieldNames = fields.keySet(); - Optional isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(),fields); + Optional isNotSensible = Introspection.isIntrospectionSensible(fields, executionContext); if (isNotSensible.isPresent()) { return CompletableFuture.completedFuture(isNotSensible.get()); } diff --git a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java index 7242578e89..91d2eadeac 100644 --- a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java @@ -43,7 +43,7 @@ public CompletableFuture execute(ExecutionContext executionCont // this is highly unlikely since Mutations cant do introspection BUT in theory someone could make the query strategy this code // so belts and braces - Optional isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(), fields); + Optional isNotSensible = Introspection.isIntrospectionSensible(fields, executionContext); if (isNotSensible.isPresent()) { return CompletableFuture.completedFuture(isNotSensible.get()); } diff --git a/src/main/java/graphql/introspection/GoodFaithIntrospection.java b/src/main/java/graphql/introspection/GoodFaithIntrospection.java new file mode 100644 index 0000000000..6c200d21f0 --- /dev/null +++ b/src/main/java/graphql/introspection/GoodFaithIntrospection.java @@ -0,0 +1,125 @@ +package graphql.introspection; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import graphql.ErrorClassification; +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.GraphQLContext; +import graphql.GraphQLError; +import graphql.PublicApi; +import graphql.execution.ExecutionContext; +import graphql.language.SourceLocation; +import graphql.normalized.ExecutableNormalizedField; +import graphql.normalized.ExecutableNormalizedOperation; +import graphql.schema.FieldCoordinates; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +import static graphql.schema.FieldCoordinates.coordinates; + +/** + * This {@link graphql.execution.instrumentation.Instrumentation} ensure that a submitted introspection query is done in + * good faith. + *

+ * There are attack vectors where a crafted introspection query can cause the engine to spend too much time + * producing introspection data. This is especially true on large schemas with lots of types and fields. + *

+ * Schemas form a cyclic graph and hence it's possible to send in introspection queries that can reference those cycles + * and in large schemas this can be expensive and perhaps a "denial of service". + *

+ * This instrumentation only allows one __schema field or one __type field to be present, and it does not allow the `__Type` fields + * to form a cycle, i.e., that can only be present once. This allows the standard and common introspection queries to work + * so tooling such as graphiql can work. + */ +@PublicApi +public class GoodFaithIntrospection { + + /** + * Placing a boolean value under this key in the per request {@link GraphQLContext} will enable + * or disable Good Faith Introspection on that request. + */ + public static final String GOOD_FAITH_INTROSPECTION_DISABLED = "GOOD_FAITH_INTROSPECTION_DISABLED"; + + private static final AtomicBoolean ENABLED_STATE = new AtomicBoolean(true); + + /** + * @return true if good faith introspection is enabled + */ + public static boolean isEnabledJvmWide() { + return ENABLED_STATE.get(); + } + + /** + * This allows you to disable good faith introspection, which is on by default. + * + * @param flag the desired state + * + * @return the previous state + */ + public static boolean enabledJvmWide(boolean flag) { + return ENABLED_STATE.getAndSet(flag); + } + + private static final Map ALLOWED_FIELD_INSTANCES = new HashMap<>(); + + static { + ALLOWED_FIELD_INSTANCES.put(coordinates("Query", "__schema"), 1); + ALLOWED_FIELD_INSTANCES.put(coordinates("Query", "__type"), 1); + ALLOWED_FIELD_INSTANCES.put(coordinates("__Type", "fields"), 1); + ALLOWED_FIELD_INSTANCES.put(coordinates("__Type", "inputFields"), 1); + ALLOWED_FIELD_INSTANCES.put(coordinates("__Type", "interfaces"), 1); + ALLOWED_FIELD_INSTANCES.put(coordinates("__Type", "possibleTypes"), 1); + } + + public static Optional checkIntrospection(ExecutionContext executionContext) { + if (isIntrospectionEnabled(executionContext.getGraphQLContext())) { + ExecutableNormalizedOperation operation = executionContext.getNormalizedQueryTree().get(); + ImmutableListMultimap coordinatesToENFs = operation.getCoordinatesToNormalizedFields(); + for (Map.Entry entry : ALLOWED_FIELD_INSTANCES.entrySet()) { + FieldCoordinates coordinates = entry.getKey(); + Integer allowSize = entry.getValue(); + ImmutableList normalizedFields = coordinatesToENFs.get(coordinates); + if (normalizedFields.size() > allowSize) { + BadFaithIntrospectionError error = new BadFaithIntrospectionError(coordinates.toString()); + return Optional.of(ExecutionResultImpl.newExecutionResult().addError(error).build()); + } + } + } + return Optional.empty(); + } + + private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) { + if (!isEnabledJvmWide()) { + return false; + } + return !graphQlContext.getOrDefault(GOOD_FAITH_INTROSPECTION_DISABLED, false); + } + + public static class BadFaithIntrospectionError implements GraphQLError { + private final String message; + + public BadFaithIntrospectionError(String qualifiedField) { + this.message = String.format("This request is not asking for introspection in good faith - %s is present too often!", qualifiedField); + } + + @Override + public String getMessage() { + return message; + } + + @Override + public ErrorClassification getErrorType() { + return ErrorClassification.errorClassification("BadFaithIntrospection"); + } + + @Override + public List getLocations() { + return null; + } + } +} diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index 3c088d5eaf..9c37598f33 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -8,6 +8,7 @@ import graphql.GraphQLContext; import graphql.Internal; import graphql.PublicApi; +import graphql.execution.ExecutionContext; import graphql.execution.MergedField; import graphql.execution.MergedSelectionSet; import graphql.execution.ValuesResolver; @@ -108,10 +109,12 @@ public static boolean isEnabledJvmWide() { * that can be returned to the user. * * @param mergedSelectionSet the fields to be executed + * @param executionContext the execution context in play * * @return an optional error result */ - public static Optional isIntrospectionSensible(GraphQLContext graphQLContext, MergedSelectionSet mergedSelectionSet) { + public static Optional isIntrospectionSensible(MergedSelectionSet mergedSelectionSet, ExecutionContext executionContext) { + GraphQLContext graphQLContext = executionContext.getGraphQLContext(); MergedField schemaField = mergedSelectionSet.getSubField(SchemaMetaFieldDef.getName()); if (schemaField != null) { if (!isIntrospectionEnabled(graphQLContext)) { @@ -124,7 +127,10 @@ public static Optional isIntrospectionSensible(GraphQLContext g return mkDisabledError(typeField); } } - // later we can put a good faith check code here to check the fields make sense + if (schemaField != null || typeField != null) + { + return GoodFaithIntrospection.checkIntrospection(executionContext); + } return Optional.empty(); } diff --git a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy new file mode 100644 index 0000000000..f1ffc2c570 --- /dev/null +++ b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy @@ -0,0 +1,136 @@ +package graphql.introspection + +import graphql.ExecutionInput +import graphql.ExecutionResult +import graphql.TestUtil +import spock.lang.Specification + +class GoodFaithIntrospectionInstrumentationTest extends Specification { + + def graphql = TestUtil.graphQL("type Query { normalField : String }").build() + + def setup() { + GoodFaithIntrospection.enabledJvmWide(true) + } + def cleanup() { + GoodFaithIntrospection.enabledJvmWide(true) + } + + def "test asking for introspection in good faith"() { + + when: + ExecutionResult er = graphql.execute(IntrospectionQuery.INTROSPECTION_QUERY) + then: + er.errors.isEmpty() + } + + def "test asking for introspection in bad faith"() { + + when: + ExecutionResult er = graphql.execute(query) + then: + !er.errors.isEmpty() + er.errors[0] instanceof GoodFaithIntrospection.BadFaithIntrospectionError + + where: + query | _ + // long attack + """ + query badActor{__schema{types{fields{type{fields{type{fields{type{fields{type{name}}}}}}}}}}} + """ | _ + // a case for __Type interfaces + """ query badActor { + __schema { types { interfaces { fields { type { interfaces { name } } } } } } + } + """ | _ + // a case for __Type inputFields + """ query badActor { + __schema { types { inputFields { type { inputFields { name }}}}} + } + """ | _ + // a case for __Type possibleTypes + """ query badActor { + __schema { types { inputFields { type { inputFields { name }}}}} + } + """ | _ + // a case leading from __InputValue + """ query badActor { + __schema { types { fields { args { type { name fields { name }}}}}} + } + """ | _ + // a case leading from __Field + """ query badActor { + __schema { types { fields { type { name fields { name }}}}} + } + """ | _ + // a case for __type + """ query badActor { + __type(name : "t") { name } + alias1 : __type(name : "t1") { name } + } + """ | _ + // a case for schema repeated - dont ask twice + """ query badActor { + __schema { types { name} } + alias1 : __schema { types { name} } + } + """ | _ + } + + def "mixed general queries and introspections will be stopped anyway"() { + def query = """ + query goodAndBad { + normalField + __schema{types{fields{type{fields{type{fields{type{fields{type{name}}}}}}}}}} + } + """ + + when: + ExecutionResult er = graphql.execute(query) + then: + !er.errors.isEmpty() + er.errors[0] instanceof GoodFaithIntrospection.BadFaithIntrospectionError + er.data == null // it stopped hard - it did not continue to normal business + } + + def "can be disabled"() { + when: + def currentState = GoodFaithIntrospection.isEnabledJvmWide() + + then: + currentState + + when: + def prevState = GoodFaithIntrospection.enabledJvmWide(false) + + then: + prevState + + when: + ExecutionResult er = graphql.execute("query badActor{__schema{types{fields{type{fields{type{fields{type{fields{type{name}}}}}}}}}}}") + + then: + er.errors.isEmpty() + } + + def "can be disabled per request"() { + when: + def context = [(GoodFaithIntrospection.GOOD_FAITH_INTROSPECTION_DISABLED): true] + ExecutionInput executionInput = ExecutionInput.newExecutionInput("query badActor{__schema{types{fields{type{fields{type{fields{type{fields{type{name}}}}}}}}}}}") + .graphQLContext(context).build() + ExecutionResult er = graphql.execute(executionInput) + + then: + er.errors.isEmpty() + + when: + context = [(GoodFaithIntrospection.GOOD_FAITH_INTROSPECTION_DISABLED): false] + executionInput = ExecutionInput.newExecutionInput("query badActor{__schema{types{fields{type{fields{type{fields{type{fields{type{name}}}}}}}}}}}") + .graphQLContext(context).build() + er = graphql.execute(executionInput) + + then: + !er.errors.isEmpty() + er.errors[0] instanceof GoodFaithIntrospection.BadFaithIntrospectionError + } +} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index 726a98eb63..6568880118 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -1324,17 +1324,10 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { ''' } - def "introspection query can be printed"() { + def "introspection query can be printed __schema"() { def sdl = ''' type Query { - foo1: Foo - } - interface Foo { - test: String - } - type AFoo implements Foo { - test: String - aFoo: String + f: String } ''' def query = ''' @@ -1346,14 +1339,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { } } } - - __type(name: "World") { - name - fields { - name - } - } - } + } ''' GraphQLSchema schema = mkSchema(sdl) @@ -1370,6 +1356,34 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { } } } +} +''' + } + + def "introspection query can be printed __type"() { + def sdl = ''' + type Query { + f: String + } + ''' + def query = ''' + query introspection_query { + __type(name: "World") { + name + fields { + name + } + } + } + ''' + + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + documentPrinted == '''{ __type(name: "World") { fields { name