-
Notifications
You must be signed in to change notification settings - Fork 1.2k
19.x Backport #3526 and PR #3527 #3532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| * <p> | ||
| * 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. | ||
| * <p> | ||
| * 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". | ||
| * <p> | ||
| * 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<FieldCoordinates, Integer> 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); | ||
| } | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Java 8 alert: there is no Map.of until Java 9 so I am using a static block like a caveman |
||
|
|
||
| public static Optional<ExecutionResult> checkIntrospection(ExecutionContext executionContext) { | ||
| if (isIntrospectionEnabled(executionContext.getGraphQLContext())) { | ||
| ExecutableNormalizedOperation operation = executionContext.getNormalizedQueryTree().get(); | ||
| ImmutableListMultimap<FieldCoordinates, ExecutableNormalizedField> coordinatesToENFs = operation.getCoordinatesToNormalizedFields(); | ||
| for (Map.Entry<FieldCoordinates, Integer> entry : ALLOWED_FIELD_INSTANCES.entrySet()) { | ||
| FieldCoordinates coordinates = entry.getKey(); | ||
| Integer allowSize = entry.getValue(); | ||
| ImmutableList<ExecutableNormalizedField> normalizedFields = coordinatesToENFs.get(coordinates); | ||
| if (normalizedFields.size() > allowSize) { | ||
| BadFaithIntrospectionError error = new BadFaithIntrospectionError(coordinates.toString()); | ||
| return Optional.of(ExecutionResultImpl.newExecutionResult().addError(error).build()); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Slight difference to master: this builder has changed file location For v19 it is inside |
||
| } | ||
| } | ||
| } | ||
| 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<SourceLocation> getLocations() { | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,8 +3,14 @@ | |
|
|
||
| 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.ExecutionContext; | ||
| import graphql.execution.MergedField; | ||
| import graphql.execution.MergedSelectionSet; | ||
| import graphql.execution.ValuesResolver; | ||
| import graphql.language.AstPrinter; | ||
| import graphql.schema.FieldCoordinates; | ||
|
|
@@ -31,13 +37,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 +65,88 @@ | |
| import static graphql.schema.GraphQLTypeUtil.unwrapOne; | ||
| import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; | ||
|
|
||
| /** | ||
| * GraphQl has a unique capability called <a href="https://spec.graphql.org/October2021/#sec-Introspection">Introspection</a> that allow | ||
| * consumers to inspect the system and discover the fields and types available and makes the system self documented. | ||
| * <p> | ||
| * Some security recommendations such as <a href="https://owasp.org/www-chapter-vancouver/assets/presentations/2020-06_GraphQL_Security.pdf">OWASP</a> | ||
| * 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 | ||
| * @param executionContext the execution context in play | ||
| * | ||
| * @return an optional error result | ||
| */ | ||
| public static Optional<ExecutionResult> isIntrospectionSensible(MergedSelectionSet mergedSelectionSet, ExecutionContext executionContext) { | ||
| GraphQLContext graphQLContext = executionContext.getGraphQLContext(); | ||
| 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); | ||
| } | ||
| } | ||
| if (schemaField != null || typeField != null) | ||
| { | ||
| return GoodFaithIntrospection.checkIntrospection(executionContext); | ||
| } | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| @NotNull | ||
| private static Optional<ExecutionResult> mkDisabledError(MergedField schemaField) { | ||
| IntrospectionDisabledError error = new IntrospectionDisabledError(schemaField.getSingleField().getSourceLocation()); | ||
| return Optional.of(ExecutionResultImpl.newExecutionResult().addError(error).build()); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. v19 API difference: this builder method is moved in v20 onwards |
||
| } | ||
|
|
||
| private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) { | ||
| if (!isEnabledJvmWide()) { | ||
| return false; | ||
| } | ||
| return !graphQlContext.getOrDefault(INTROSPECTION_DISABLED, false); | ||
| } | ||
|
|
||
| private static final Map<FieldCoordinates, IntrospectionDataFetcher<?>> introspectionDataFetchers = new LinkedHashMap<>(); | ||
|
|
||
| private static void register(GraphQLFieldsContainer parentType, String fieldName, IntrospectionDataFetcher<?> introspectionDataFetcher) { | ||
|
|
@@ -592,6 +681,7 @@ public enum DirectiveLocation { | |
| return environment.getGraphQLSchema().getType(name); | ||
| }; | ||
|
|
||
| // __typename is always available | ||
| public static final IntrospectionDataFetcher<?> TypeNameMetaFieldDefDataFetcher = environment -> simplePrint(environment.getParentType()); | ||
|
|
||
| @Internal | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<SourceLocation> 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<SourceLocation> getLocations() { | ||
| return locations; | ||
| } | ||
|
|
||
| @Override | ||
| public ErrorClassification getErrorType() { | ||
| return ErrorClassification.errorClassification("IntrospectionDisabled"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 <a href="https://spec.graphql.org/October2021/#sec-Introspection">the specification</a> | ||
| * 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") | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Java 8 special: since is not an argument for the Deprecated annotation, so it's a comment This was added in Java 9. v21 uses Java 11.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice |
||
| public static NoIntrospectionGraphqlFieldVisibility NO_INTROSPECTION_FIELD_VISIBILITY = new NoIntrospectionGraphqlFieldVisibility(); | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also backported this convenience method to make the backport easier - it's in v20 onwards