Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/main/java/graphql/execution/AsyncExecutionStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters;
import graphql.introspection.Introspection;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;

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

Optional<ExecutionResult> isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(),fields);
if (isNotSensible.isPresent()) {
return CompletableFuture.completedFuture(isNotSensible.get());
}

DeferredExecutionSupport deferredExecutionSupport = createDeferredExecutionSupport(executionContext, parameters);
Async.CombinedBuilder<FieldValueInfo> futures = getAsyncFieldValueInfo(executionContext, parameters, deferredExecutionSupport);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,6 +42,13 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
MergedSelectionSet fields = parameters.getFields();
ImmutableList<String> 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<ExecutionResult> isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(), fields);
if (isNotSensible.isPresent()) {
return CompletableFuture.completedFuture(isNotSensible.get());
}

CompletableFuture<List<Object>> resultsFuture = Async.eachSequentially(fieldNames, (fieldName, prevResults) -> {
MergedField currentField = fields.getSubField(fieldName);
ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField));
Expand Down
82 changes: 82 additions & 0 deletions src/main/java/graphql/introspection/Introspection.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

import com.google.common.collect.ImmutableSet;
import graphql.Assert;
import graphql.ExecutionResult;
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;
Expand All @@ -32,14 +35,17 @@
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.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;

Expand All @@ -58,8 +64,83 @@
import static graphql.schema.GraphQLTypeUtil.unwrapAllAs;
import static graphql.schema.GraphQLTypeUtil.unwrapOne;

/**
* 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
*
* @return an optional error result
*/
public static Optional<ExecutionResult> 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<ExecutionResult> mkDisabledError(MergedField schemaField) {
IntrospectionDisabledError error = new IntrospectionDisabledError(schemaField.getSingleField().getSourceLocation());
return Optional.of(ExecutionResult.newExecutionResult().addError(error).build());
}

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) {
Expand Down Expand Up @@ -636,6 +717,7 @@ public enum DirectiveLocation {
return environment.getGraphQLSchema().getType(name);
};

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

@Internal
Expand Down
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
Expand Up @@ -12,10 +12,15 @@
* This field visibility will prevent Introspection queries from being performed. Technically this puts your
* 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")
public static NoIntrospectionGraphqlFieldVisibility NO_INTROSPECTION_FIELD_VISIBILITY = new NoIntrospectionGraphqlFieldVisibility();


Expand Down
120 changes: 118 additions & 2 deletions src/test/groovy/graphql/introspection/IntrospectionTest.groovy
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,6 +23,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 {
Expand Down Expand Up @@ -547,7 +556,7 @@ class IntrospectionTest extends Specification {

then:
def oldQuery = oldIntrospectionQuery.replaceAll("\\s+", "")
def newQuery = newIntrospectionQuery.replaceAll("\\s+","")
def newQuery = newIntrospectionQuery.replaceAll("\\s+", "")
oldQuery == newQuery
}

Expand Down Expand Up @@ -688,4 +697,111 @@ class IntrospectionTest extends Specification {
queryType["isOneOf"] == null
}

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(Map.of(Introspection.INTROSPECTION_DISABLED, false)).build()
er = graphQL.execute(ei)

then:
er.errors.isEmpty()

when:
ei = ExecutionInput.newExecutionInput(IntrospectionQuery.INTROSPECTION_QUERY)
.graphQLContext(Map.of(Introspection.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()
}

}