Skip to content

Commit b0127e4

Browse files
authored
Merge pull request #3530 from graphql-java/20.x-backport-3526-disable-introspection
20.x Backport of PR 3526 and PR 3527
2 parents 8d42f76 + 15c0380 commit b0127e4

9 files changed

+557
-20
lines changed

src/main/java/graphql/execution/AsyncExecutionStrategy.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext;
66
import graphql.execution.instrumentation.Instrumentation;
77
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters;
8+
import graphql.introspection.Introspection;
89

910
import java.util.List;
11+
import java.util.Optional;
1012
import java.util.concurrent.CompletableFuture;
1113
import java.util.function.BiConsumer;
1214

@@ -44,6 +46,12 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
4446

4547
MergedSelectionSet fields = parameters.getFields();
4648
List<String> fieldNames = fields.getKeys();
49+
50+
Optional<ExecutionResult> isNotSensible = Introspection.isIntrospectionSensible(fields, executionContext);
51+
if (isNotSensible.isPresent()) {
52+
return CompletableFuture.completedFuture(isNotSensible.get());
53+
}
54+
4755
Async.CombinedBuilder<FieldValueInfo> futures = Async.ofExpectedSize(fields.size());
4856
for (String fieldName : fieldNames) {
4957
MergedField currentField = fields.getSubField(fieldName);

src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import graphql.execution.instrumentation.Instrumentation;
77
import graphql.execution.instrumentation.InstrumentationContext;
88
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters;
9+
import graphql.introspection.Introspection;
910

1011
import java.util.List;
12+
import java.util.Optional;
1113
import java.util.concurrent.CompletableFuture;
1214

1315
import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx;
@@ -39,6 +41,13 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
3941
MergedSelectionSet fields = parameters.getFields();
4042
ImmutableList<String> fieldNames = ImmutableList.copyOf(fields.keySet());
4143

44+
// this is highly unlikely since Mutations cant do introspection BUT in theory someone could make the query strategy this code
45+
// so belts and braces
46+
Optional<ExecutionResult> isNotSensible = Introspection.isIntrospectionSensible(fields, executionContext);
47+
if (isNotSensible.isPresent()) {
48+
return CompletableFuture.completedFuture(isNotSensible.get());
49+
}
50+
4251
CompletableFuture<List<ExecutionResult>> resultsFuture = Async.eachSequentially(fieldNames, (fieldName, index, prevResults) -> {
4352
MergedField currentField = fields.getSubField(fieldName);
4453
ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField));
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package graphql.introspection;
2+
3+
import com.google.common.collect.ImmutableList;
4+
import com.google.common.collect.ImmutableListMultimap;
5+
import graphql.ErrorClassification;
6+
import graphql.ExecutionResult;
7+
import graphql.GraphQLContext;
8+
import graphql.GraphQLError;
9+
import graphql.PublicApi;
10+
import graphql.execution.ExecutionContext;
11+
import graphql.language.SourceLocation;
12+
import graphql.normalized.ExecutableNormalizedField;
13+
import graphql.normalized.ExecutableNormalizedOperation;
14+
import graphql.schema.FieldCoordinates;
15+
16+
import java.util.HashMap;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.Optional;
20+
import java.util.concurrent.atomic.AtomicBoolean;
21+
22+
import static graphql.schema.FieldCoordinates.coordinates;
23+
24+
/**
25+
* This {@link graphql.execution.instrumentation.Instrumentation} ensure that a submitted introspection query is done in
26+
* good faith.
27+
* <p>
28+
* There are attack vectors where a crafted introspection query can cause the engine to spend too much time
29+
* producing introspection data. This is especially true on large schemas with lots of types and fields.
30+
* <p>
31+
* Schemas form a cyclic graph and hence it's possible to send in introspection queries that can reference those cycles
32+
* and in large schemas this can be expensive and perhaps a "denial of service".
33+
* <p>
34+
* This instrumentation only allows one __schema field or one __type field to be present, and it does not allow the `__Type` fields
35+
* to form a cycle, i.e., that can only be present once. This allows the standard and common introspection queries to work
36+
* so tooling such as graphiql can work.
37+
*/
38+
@PublicApi
39+
public class GoodFaithIntrospection {
40+
41+
/**
42+
* Placing a boolean value under this key in the per request {@link GraphQLContext} will enable
43+
* or disable Good Faith Introspection on that request.
44+
*/
45+
public static final String GOOD_FAITH_INTROSPECTION_DISABLED = "GOOD_FAITH_INTROSPECTION_DISABLED";
46+
47+
private static final AtomicBoolean ENABLED_STATE = new AtomicBoolean(true);
48+
49+
/**
50+
* @return true if good faith introspection is enabled
51+
*/
52+
public static boolean isEnabledJvmWide() {
53+
return ENABLED_STATE.get();
54+
}
55+
56+
/**
57+
* This allows you to disable good faith introspection, which is on by default.
58+
*
59+
* @param flag the desired state
60+
*
61+
* @return the previous state
62+
*/
63+
public static boolean enabledJvmWide(boolean flag) {
64+
return ENABLED_STATE.getAndSet(flag);
65+
}
66+
67+
private static final Map<FieldCoordinates, Integer> ALLOWED_FIELD_INSTANCES = new HashMap<>();
68+
69+
static {
70+
ALLOWED_FIELD_INSTANCES.put(coordinates("Query", "__schema"), 1);
71+
ALLOWED_FIELD_INSTANCES.put(coordinates("Query", "__type"), 1);
72+
ALLOWED_FIELD_INSTANCES.put(coordinates("__Type", "fields"), 1);
73+
ALLOWED_FIELD_INSTANCES.put(coordinates("__Type", "inputFields"), 1);
74+
ALLOWED_FIELD_INSTANCES.put(coordinates("__Type", "interfaces"), 1);
75+
ALLOWED_FIELD_INSTANCES.put(coordinates("__Type", "possibleTypes"), 1);
76+
}
77+
78+
public static Optional<ExecutionResult> checkIntrospection(ExecutionContext executionContext) {
79+
if (isIntrospectionEnabled(executionContext.getGraphQLContext())) {
80+
ExecutableNormalizedOperation operation = executionContext.getNormalizedQueryTree().get();
81+
ImmutableListMultimap<FieldCoordinates, ExecutableNormalizedField> coordinatesToENFs = operation.getCoordinatesToNormalizedFields();
82+
for (Map.Entry<FieldCoordinates, Integer> entry : ALLOWED_FIELD_INSTANCES.entrySet()) {
83+
FieldCoordinates coordinates = entry.getKey();
84+
Integer allowSize = entry.getValue();
85+
ImmutableList<ExecutableNormalizedField> normalizedFields = coordinatesToENFs.get(coordinates);
86+
if (normalizedFields.size() > allowSize) {
87+
BadFaithIntrospectionError error = new BadFaithIntrospectionError(coordinates.toString());
88+
return Optional.of(ExecutionResult.newExecutionResult().addError(error).build());
89+
}
90+
}
91+
}
92+
return Optional.empty();
93+
}
94+
95+
private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) {
96+
if (!isEnabledJvmWide()) {
97+
return false;
98+
}
99+
return !graphQlContext.getOrDefault(GOOD_FAITH_INTROSPECTION_DISABLED, false);
100+
}
101+
102+
public static class BadFaithIntrospectionError implements GraphQLError {
103+
private final String message;
104+
105+
public BadFaithIntrospectionError(String qualifiedField) {
106+
this.message = String.format("This request is not asking for introspection in good faith - %s is present too often!", qualifiedField);
107+
}
108+
109+
@Override
110+
public String getMessage() {
111+
return message;
112+
}
113+
114+
@Override
115+
public ErrorClassification getErrorType() {
116+
return ErrorClassification.errorClassification("BadFaithIntrospection");
117+
}
118+
119+
@Override
120+
public List<SourceLocation> getLocations() {
121+
return null;
122+
}
123+
}
124+
}

src/main/java/graphql/introspection/Introspection.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33

44
import com.google.common.collect.ImmutableSet;
55
import graphql.Assert;
6+
import graphql.ExecutionResult;
67
import graphql.GraphQLContext;
78
import graphql.Internal;
89
import graphql.PublicApi;
10+
import graphql.execution.ExecutionContext;
11+
import graphql.execution.MergedField;
12+
import graphql.execution.MergedSelectionSet;
913
import graphql.execution.ValuesResolver;
1014
import graphql.language.AstPrinter;
1115
import graphql.schema.FieldCoordinates;
@@ -32,14 +36,17 @@
3236
import graphql.schema.GraphQLSchema;
3337
import graphql.schema.GraphQLUnionType;
3438
import graphql.schema.InputValueWithState;
39+
import org.jetbrains.annotations.NotNull;
3540

3641
import java.util.ArrayList;
3742
import java.util.HashSet;
3843
import java.util.LinkedHashMap;
3944
import java.util.List;
4045
import java.util.Locale;
4146
import java.util.Map;
47+
import java.util.Optional;
4248
import java.util.Set;
49+
import java.util.concurrent.atomic.AtomicBoolean;
4350
import java.util.function.Function;
4451
import java.util.stream.Collectors;
4552

@@ -58,8 +65,88 @@
5865
import static graphql.schema.GraphQLTypeUtil.unwrapAllAs;
5966
import static graphql.schema.GraphQLTypeUtil.unwrapOne;
6067

68+
/**
69+
* GraphQl has a unique capability called <a href="https://spec.graphql.org/October2021/#sec-Introspection">Introspection</a> that allow
70+
* consumers to inspect the system and discover the fields and types available and makes the system self documented.
71+
* <p>
72+
* Some security recommendations such as <a href="https://owasp.org/www-chapter-vancouver/assets/presentations/2020-06_GraphQL_Security.pdf">OWASP</a>
73+
* recommend that introspection be disabled in production. The {@link Introspection#enabledJvmWide(boolean)} method can be used to disable
74+
* introspection for the whole JVM or you can place {@link Introspection#INTROSPECTION_DISABLED} into the {@link GraphQLContext} of a request
75+
* to disable introspection for that request.
76+
*/
6177
@PublicApi
6278
public class Introspection {
79+
80+
81+
/**
82+
* Placing a boolean value under this key in the per request {@link GraphQLContext} will enable
83+
* or disable Introspection on that request.
84+
*/
85+
public static final String INTROSPECTION_DISABLED = "INTROSPECTION_DISABLED";
86+
private static final AtomicBoolean INTROSPECTION_ENABLED_STATE = new AtomicBoolean(true);
87+
88+
/**
89+
* This static method will enable / disable Introspection at a JVM wide level.
90+
*
91+
* @param enabled the flag indicating the desired enabled state
92+
*
93+
* @return the previous state of enablement
94+
*/
95+
public static boolean enabledJvmWide(boolean enabled) {
96+
return INTROSPECTION_ENABLED_STATE.getAndSet(enabled);
97+
}
98+
99+
/**
100+
* @return true if Introspection is enabled at a JVM wide level or false otherwise
101+
*/
102+
public static boolean isEnabledJvmWide() {
103+
return INTROSPECTION_ENABLED_STATE.get();
104+
}
105+
106+
/**
107+
* This will look in to the field selection set and see if there are introspection fields,
108+
* and if there is,it checks if introspection should run, and if not it will return an errored {@link ExecutionResult}
109+
* that can be returned to the user.
110+
*
111+
* @param mergedSelectionSet the fields to be executed
112+
* @param executionContext the execution context in play
113+
*
114+
* @return an optional error result
115+
*/
116+
public static Optional<ExecutionResult> isIntrospectionSensible(MergedSelectionSet mergedSelectionSet, ExecutionContext executionContext) {
117+
GraphQLContext graphQLContext = executionContext.getGraphQLContext();
118+
MergedField schemaField = mergedSelectionSet.getSubField(SchemaMetaFieldDef.getName());
119+
if (schemaField != null) {
120+
if (!isIntrospectionEnabled(graphQLContext)) {
121+
return mkDisabledError(schemaField);
122+
}
123+
}
124+
MergedField typeField = mergedSelectionSet.getSubField(TypeMetaFieldDef.getName());
125+
if (typeField != null) {
126+
if (!isIntrospectionEnabled(graphQLContext)) {
127+
return mkDisabledError(typeField);
128+
}
129+
}
130+
if (schemaField != null || typeField != null)
131+
{
132+
return GoodFaithIntrospection.checkIntrospection(executionContext);
133+
}
134+
return Optional.empty();
135+
}
136+
137+
@NotNull
138+
private static Optional<ExecutionResult> mkDisabledError(MergedField schemaField) {
139+
IntrospectionDisabledError error = new IntrospectionDisabledError(schemaField.getSingleField().getSourceLocation());
140+
return Optional.of(ExecutionResult.newExecutionResult().addError(error).build());
141+
}
142+
143+
private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) {
144+
if (!isEnabledJvmWide()) {
145+
return false;
146+
}
147+
return !graphQlContext.getOrDefault(INTROSPECTION_DISABLED, false);
148+
}
149+
63150
private static final Map<FieldCoordinates, IntrospectionDataFetcher<?>> introspectionDataFetchers = new LinkedHashMap<>();
64151

65152
private static void register(GraphQLFieldsContainer parentType, String fieldName, IntrospectionDataFetcher<?> introspectionDataFetcher) {
@@ -623,6 +710,7 @@ public enum DirectiveLocation {
623710
return environment.getGraphQLSchema().getType(name);
624711
};
625712

713+
// __typename is always available
626714
public static final IntrospectionDataFetcher<?> TypeNameMetaFieldDefDataFetcher = environment -> simplePrint(environment.getParentType());
627715

628716
@Internal
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package graphql.introspection;
2+
3+
import graphql.ErrorClassification;
4+
import graphql.ErrorType;
5+
import graphql.GraphQLError;
6+
import graphql.Internal;
7+
import graphql.language.SourceLocation;
8+
9+
import java.util.Collections;
10+
import java.util.List;
11+
12+
@Internal
13+
public class IntrospectionDisabledError implements GraphQLError {
14+
15+
private final List<SourceLocation> locations;
16+
17+
public IntrospectionDisabledError(SourceLocation sourceLocation) {
18+
locations = sourceLocation == null ? Collections.emptyList() : Collections.singletonList(sourceLocation);
19+
}
20+
21+
@Override
22+
public String getMessage() {
23+
return "Introspection has been disabled for this request";
24+
}
25+
26+
@Override
27+
public List<SourceLocation> getLocations() {
28+
return locations;
29+
}
30+
31+
@Override
32+
public ErrorClassification getErrorType() {
33+
return ErrorClassification.errorClassification("IntrospectionDisabled");
34+
}
35+
}

src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@
1212
* This field visibility will prevent Introspection queries from being performed. Technically this puts your
1313
* system in contravention of <a href="https://spec.graphql.org/October2021/#sec-Introspection">the specification</a>
1414
* but some production systems want this lock down in place.
15+
*
16+
* @deprecated This is no longer the best way to prevent Introspection - {@link graphql.introspection.Introspection#enabledJvmWide(boolean)}
17+
* can be used instead
1518
*/
1619
@PublicApi
20+
@Deprecated // Deprecated since 2024-03-16
1721
public class NoIntrospectionGraphqlFieldVisibility implements GraphqlFieldVisibility {
1822

23+
@Deprecated // Deprecated since 2024-03-16
1924
public static NoIntrospectionGraphqlFieldVisibility NO_INTROSPECTION_FIELD_VISIBILITY = new NoIntrospectionGraphqlFieldVisibility();
2025

2126

0 commit comments

Comments
 (0)