Skip to content

Commit 776de2f

Browse files
authored
Merge pull request #3526 from graphql-java/ability_to_disable_introspection
This provides and ability to disable Introspection
2 parents c92d0f2 + 498175a commit 776de2f

6 files changed

Lines changed: 256 additions & 2 deletions

File tree

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext;
77
import graphql.execution.instrumentation.Instrumentation;
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
import java.util.function.BiConsumer;
1315

@@ -46,6 +48,11 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
4648
MergedSelectionSet fields = parameters.getFields();
4749
List<String> fieldNames = fields.getKeys();
4850

51+
Optional<ExecutionResult> isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(),fields);
52+
if (isNotSensible.isPresent()) {
53+
return CompletableFuture.completedFuture(isNotSensible.get());
54+
}
55+
4956
DeferredExecutionSupport deferredExecutionSupport = createDeferredExecutionSupport(executionContext, parameters);
5057
Async.CombinedBuilder<FieldValueInfo> futures = getAsyncFieldValueInfo(executionContext, parameters, deferredExecutionSupport);
5158

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;
@@ -40,6 +42,13 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
4042
MergedSelectionSet fields = parameters.getFields();
4143
ImmutableList<String> fieldNames = ImmutableList.copyOf(fields.keySet());
4244

45+
// this is highly unlikely since Mutations cant do introspection BUT in theory someone could make the query strategy this code
46+
// so belts and braces
47+
Optional<ExecutionResult> isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(), fields);
48+
if (isNotSensible.isPresent()) {
49+
return CompletableFuture.completedFuture(isNotSensible.get());
50+
}
51+
4352
CompletableFuture<List<Object>> resultsFuture = Async.eachSequentially(fieldNames, (fieldName, prevResults) -> {
4453
MergedField currentField = fields.getSubField(fieldName);
4554
ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField));

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
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.MergedField;
11+
import graphql.execution.MergedSelectionSet;
912
import graphql.execution.ValuesResolver;
1013
import graphql.language.AstPrinter;
1114
import graphql.schema.FieldCoordinates;
@@ -32,14 +35,17 @@
3235
import graphql.schema.GraphQLSchema;
3336
import graphql.schema.GraphQLUnionType;
3437
import graphql.schema.InputValueWithState;
38+
import org.jetbrains.annotations.NotNull;
3539

3640
import java.util.ArrayList;
3741
import java.util.HashSet;
3842
import java.util.LinkedHashMap;
3943
import java.util.List;
4044
import java.util.Locale;
4145
import java.util.Map;
46+
import java.util.Optional;
4247
import java.util.Set;
48+
import java.util.concurrent.atomic.AtomicBoolean;
4349
import java.util.function.Function;
4450
import java.util.stream.Collectors;
4551

@@ -58,8 +64,83 @@
5864
import static graphql.schema.GraphQLTypeUtil.unwrapAllAs;
5965
import static graphql.schema.GraphQLTypeUtil.unwrapOne;
6066

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

65146
private static void register(GraphQLFieldsContainer parentType, String fieldName, IntrospectionDataFetcher<?> introspectionDataFetcher) {
@@ -636,6 +717,7 @@ public enum DirectiveLocation {
636717
return environment.getGraphQLSchema().getType(name);
637718
};
638719

720+
// __typename is always available
639721
public static final IntrospectionDataFetcher<?> TypeNameMetaFieldDefDataFetcher = environment -> simplePrint(environment.getParentType());
640722

641723
@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(since = "2024-03-16")
1721
public class NoIntrospectionGraphqlFieldVisibility implements GraphqlFieldVisibility {
1822

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

2126

src/test/groovy/graphql/introspection/IntrospectionTest.groovy

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package graphql.introspection
22

3-
3+
import graphql.ExecutionInput
44
import graphql.TestUtil
5+
import graphql.execution.AsyncSerialExecutionStrategy
56
import graphql.schema.DataFetcher
67
import graphql.schema.FieldCoordinates
78
import graphql.schema.GraphQLCodeRegistry
@@ -22,6 +23,14 @@ import static graphql.schema.GraphQLSchema.newSchema
2223

2324
class IntrospectionTest extends Specification {
2425

26+
def setup() {
27+
Introspection.enabledJvmWide(true)
28+
}
29+
30+
def cleanup() {
31+
Introspection.enabledJvmWide(true)
32+
}
33+
2534
def "bug 1186 - introspection depth check"() {
2635
def spec = '''
2736
type Query {
@@ -547,7 +556,7 @@ class IntrospectionTest extends Specification {
547556

548557
then:
549558
def oldQuery = oldIntrospectionQuery.replaceAll("\\s+", "")
550-
def newQuery = newIntrospectionQuery.replaceAll("\\s+","")
559+
def newQuery = newIntrospectionQuery.replaceAll("\\s+", "")
551560
oldQuery == newQuery
552561
}
553562

@@ -688,4 +697,111 @@ class IntrospectionTest extends Specification {
688697
queryType["isOneOf"] == null
689698
}
690699

700+
def "jvm wide enablement"() {
701+
def graphQL = TestUtil.graphQL("type Query { f : String } ").build()
702+
703+
when:
704+
def er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY)
705+
706+
then:
707+
er.errors.isEmpty()
708+
709+
when:
710+
Introspection.enabledJvmWide(false)
711+
er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY)
712+
713+
then:
714+
er.errors[0] instanceof IntrospectionDisabledError
715+
er.errors[0].getErrorType().toString() == "IntrospectionDisabled"
716+
717+
when:
718+
Introspection.enabledJvmWide(true)
719+
er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY)
720+
721+
then:
722+
er.errors.isEmpty()
723+
}
724+
725+
def "per request enablement"() {
726+
def graphQL = TestUtil.graphQL("type Query { f : String } ").build()
727+
728+
when:
729+
// null context
730+
def ei = ExecutionInput.newExecutionInput(IntrospectionQuery.INTROSPECTION_QUERY)
731+
.build()
732+
def er = graphQL.execute(ei)
733+
734+
then:
735+
er.errors.isEmpty()
736+
737+
when:
738+
ei = ExecutionInput.newExecutionInput(IntrospectionQuery.INTROSPECTION_QUERY)
739+
.graphQLContext(Map.of(Introspection.INTROSPECTION_DISABLED, false)).build()
740+
er = graphQL.execute(ei)
741+
742+
then:
743+
er.errors.isEmpty()
744+
745+
when:
746+
ei = ExecutionInput.newExecutionInput(IntrospectionQuery.INTROSPECTION_QUERY)
747+
.graphQLContext(Map.of(Introspection.INTROSPECTION_DISABLED, true)).build()
748+
er = graphQL.execute(ei)
749+
750+
then:
751+
er.errors[0] instanceof IntrospectionDisabledError
752+
er.errors[0].getErrorType().toString() == "IntrospectionDisabled"
753+
}
754+
755+
def "mixed schema and other fields stop early"() {
756+
def graphQL = TestUtil.graphQL("type Query { normalField : String } ").build()
757+
758+
def query = """
759+
query goodAndBad {
760+
normalField
761+
__schema{ types{ fields { name }}}
762+
}
763+
"""
764+
765+
when:
766+
def er = graphQL.execute(query)
767+
768+
then:
769+
er.errors.isEmpty()
770+
771+
when:
772+
Introspection.enabledJvmWide(false)
773+
er = graphQL.execute(query)
774+
775+
then:
776+
er.errors[0] instanceof IntrospectionDisabledError
777+
er.errors[0].getErrorType().toString() == "IntrospectionDisabled"
778+
er.data == null // stops hard
779+
}
780+
781+
def "AsyncSerialExecutionStrategy with jvm wide enablement"() {
782+
def graphQL = TestUtil.graphQL("type Query { f : String } ")
783+
.queryExecutionStrategy(new AsyncSerialExecutionStrategy()).build()
784+
785+
when:
786+
def er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY)
787+
788+
then:
789+
er.errors.isEmpty()
790+
791+
when:
792+
Introspection.enabledJvmWide(false)
793+
er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY)
794+
795+
then:
796+
er.errors[0] instanceof IntrospectionDisabledError
797+
er.errors[0].getErrorType().toString() == "IntrospectionDisabled"
798+
799+
when:
800+
Introspection.enabledJvmWide(true)
801+
er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY)
802+
803+
then:
804+
er.errors.isEmpty()
805+
}
806+
691807
}

0 commit comments

Comments
 (0)