Skip to content

Commit

Permalink
fix: return error when introspection query is made when disabled (wun…
Browse files Browse the repository at this point in the history
  • Loading branch information
thisisnithin authored Oct 10, 2024
1 parent 8ee36b4 commit 7d7a854
Show file tree
Hide file tree
Showing 3 changed files with 253 additions and 1 deletion.
1 change: 1 addition & 0 deletions router/core/graph_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ func (s *graphServer) buildGraphMux(ctx context.Context,
QueryDepthCache: gm.queryDepthCache,
OperationHashCache: gm.operationHashCache,
ParseKitPoolSize: s.engineExecutionConfiguration.ParseKitPoolSize,
IntrospectionEnabled: s.Config.introspection,
})
operationPlanner := NewOperationPlanner(executor, gm.planCache)

Expand Down
91 changes: 90 additions & 1 deletion router/core/operation_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"crypto/sha256"
"fmt"
"github.com/wundergraph/graphql-go-tools/v2/pkg/middleware/operation_complexity"
"hash"
"io"
"net/http"
Expand All @@ -26,6 +25,7 @@ import (
"github.com/wundergraph/graphql-go-tools/v2/pkg/astprinter"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astvalidation"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient"
"github.com/wundergraph/graphql-go-tools/v2/pkg/middleware/operation_complexity"
"github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport"
"github.com/wundergraph/graphql-go-tools/v2/pkg/variablesvalidation"
"go.opentelemetry.io/otel/attribute"
Expand Down Expand Up @@ -93,6 +93,7 @@ type OperationProcessorOptions struct {
QueryDepthCache *ristretto.Cache[uint64, int]
OperationHashCache *ristretto.Cache[uint64, string]
ParseKitPoolSize int
IntrospectionEnabled bool
}

// OperationProcessor provides shared resources to the parseKit and OperationKit.
Expand All @@ -104,6 +105,7 @@ type OperationProcessor struct {
operationCache *OperationCache
parseKits map[int]*parseKit
parseKitSemaphore chan int
introspectionEnabled bool
}

// parseKit is a helper struct to parse, normalize and validate operations
Expand Down Expand Up @@ -144,6 +146,7 @@ type OperationKit struct {
operationProcessor *OperationProcessor
kit *parseKit
parsedOperation *ParsedOperation
introspectionEnabled bool
}

type GraphQLRequest struct {
Expand Down Expand Up @@ -171,6 +174,7 @@ func NewOperationKit(processor *OperationProcessor) *OperationKit {
operationDefinitionRef: -1,
cache: processor.operationCache,
parsedOperation: &ParsedOperation{},
introspectionEnabled: processor.introspectionEnabled,
}
}

Expand Down Expand Up @@ -370,6 +374,72 @@ func (o *OperationKit) FetchPersistedOperation(ctx context.Context, clientInfo *
return false, nil
}

const (
schemaIntrospectionFieldName = "__schema"
typeIntrospectionFieldName = "__type"
)

func (o *OperationKit) isIntrospectionQuery() (result bool, err error) {
var operationDefinitionRef = ast.InvalidRef
var possibleOperationDefinitionRefs = make([]int, 0)

for i := 0; i < len(o.kit.doc.RootNodes); i++ {
if o.kit.doc.RootNodes[i].Kind == ast.NodeKindOperationDefinition {
possibleOperationDefinitionRefs = append(possibleOperationDefinitionRefs, o.kit.doc.RootNodes[i].Ref)
}
}

if len(possibleOperationDefinitionRefs) == 0 {
return
} else if len(possibleOperationDefinitionRefs) == 1 {
operationDefinitionRef = possibleOperationDefinitionRefs[0]
} else {
for i := 0; i < len(possibleOperationDefinitionRefs); i++ {
ref := possibleOperationDefinitionRefs[i]
name := o.kit.doc.OperationDefinitionNameString(ref)

if o.parsedOperation.Request.OperationName == name {
operationDefinitionRef = ref
break
}
}
}

if operationDefinitionRef == ast.InvalidRef {
return
}

operationDef := o.kit.doc.OperationDefinitions[operationDefinitionRef]
if operationDef.OperationType != ast.OperationTypeQuery {
return
}
if !operationDef.HasSelections {
return
}

selectionSet := o.kit.doc.SelectionSets[operationDef.SelectionSet]
if len(selectionSet.SelectionRefs) == 0 {
return
}

for i := 0; i < len(selectionSet.SelectionRefs); i++ {
selection := o.kit.doc.Selections[selectionSet.SelectionRefs[i]]
if selection.Kind != ast.SelectionKindField {
continue
}

fieldName := o.kit.doc.FieldNameUnsafeString(selection.Ref)
switch fieldName {
case schemaIntrospectionFieldName, typeIntrospectionFieldName:
continue
default:
return
}
}

return true, nil
}

// Parse parses the operation, populate the document and set the operation type.
// UnmarshalOperationFromBody must be called before calling this method.
func (o *OperationKit) Parse() error {
Expand All @@ -395,6 +465,24 @@ func (o *OperationKit) Parse() error {
}
}

if !o.introspectionEnabled {
isIntrospection, err := o.isIntrospectionQuery()

if err != nil {
return &httpGraphqlError{
message: "could not determine if operation was an introspection query",
statusCode: http.StatusOK,
}
}

if isIntrospection {
return &httpGraphqlError{
message: "GraphQL introspection is disabled by Cosmo Router, but the query contained __schema or __type. To enable introspection, set introspection_enabled: true in the Router configuration",
statusCode: http.StatusOK,
}
}
}

for i := range o.kit.doc.RootNodes {
if o.kit.doc.RootNodes[i].Kind != ast.NodeKindOperationDefinition {
continue
Expand Down Expand Up @@ -874,6 +962,7 @@ func NewOperationProcessor(opts OperationProcessorOptions) *OperationProcessor {
persistedOperationClient: opts.PersistedOperationClient,
parseKits: make(map[int]*parseKit, opts.ParseKitPoolSize),
parseKitSemaphore: make(chan int, opts.ParseKitPoolSize),
introspectionEnabled: opts.IntrospectionEnabled,
}
for i := 0; i < opts.ParseKitPoolSize; i++ {
processor.parseKitSemaphore <- i
Expand Down
162 changes: 162 additions & 0 deletions router/core/operation_processor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,165 @@ func TestOperationProcessorUnmarshalExtensions(t *testing.T) {
})
}
}

const namedIntrospectionQuery = `{"operationName":"IntrospectionQuery","variables":{},"query":"query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"}`
const singleNamedIntrospectionQueryWithoutOperationName = `{"operationName":"","variables":{},"query":"query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"}`
const silentIntrospectionQuery = `{"operationName":null,"variables":{},"query":"{\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"}`
const silentIntrospectionQueryWithOperationName = `{"operationName":"IntrospectionQuery","variables":{},"query":"{\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"}`
const schemaIntrospectionQueryWithMultipleQueries = `{"operationName":"IntrospectionQuery","query":"query Hello { world } query IntrospectionQuery { __schema { types { name } } }"}`
const inlineFragmentedIntrospectionQueryType = `{"operationName":"IntrospectionQuery","variables":{},"query":"query IntrospectionQuery { ... IntrospectionFragment } fragment IntrospectionFragment on Query { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description args { ...InputValue } onOperation onFragment onField } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }"}`
const inlineFragmentedIntrospectionQueryWithFragmentOnQuery = `{"operationName":"IntrospectionQuery","variables":{},"query":"query IntrospectionQuery { ... on Query { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description args { ...InputValue } onOperation onFragment onField } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }"}`
const fragmentedIntrospectionQuery = `{"operationName":"IntrospectionQuery","variables":{},"query":"query IntrospectionQuery { ... IntrospectionFragment } fragment IntrospectionFragment on Query { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description args { ...InputValue } onOperation onFragment onField } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }"}`
const typeIntrospectionQueryWithMultipleQueries = `{"operationName":"IntrospectionQuery","query":"query Hello { world } query IntrospectionQuery { __type(name: \"Droid\") { name } }"}`
const typeIntrospectionQuery = `{"operationName":null,"variables":{},"query":"{__type(name:\"Foo\"){kind}}"}`
const nonIntrospectionQuery = `{"operationName":"Foo","query":"query Foo {bar}"}`
const nonIntrospectionQueryWithIntrospectionQueryName = `{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery {bar}"}`
const nonSchemaIntrospectionQueryWithAliases = `{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery { __schema: user { name types: account { balance } } }"}`
const nonTypeIntrospectionQueryWithAliases = `{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery { __type: user { name } }"}`
const nonSchemaIntrospectionQueryWithAdditionalFields = `{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery { __schema { types { name } } user { name account { balance } } }"}`
const nonTypeIntrospectionQueryWithAdditionalFields = `{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery { __type(name: \"Droid\") { name } user { name account { balance } } }"}`
const nonSchemaIntrospectionQueryWithMultipleQueries = `{"operationName":"Hello","query":"query Hello { world } query IntrospectionQuery { __schema { types { name } } }"}`
const nonTypeIntrospectionQueryWithMultipleQueries = `{"operationName":"Hello","query":"query Hello { world } query IntrospectionQuery { __type(name: \"Droid\") { name } }"}`
const mutationQuery = `{"operationName":null,"query":"mutation Foo {bar}"}`

func TestOperationProcessorIntrospectionQuery(t *testing.T) {
executor := &Executor{
PlanConfig: plan.Configuration{},
RouterSchema: nil,
Resolver: nil,
RenameTypeNames: nil,
}
parser := NewOperationProcessor(OperationProcessorOptions{
Executor: executor,
MaxOperationSizeInBytes: 10 << 20,
ParseKitPoolSize: 4,
IntrospectionEnabled: false,
})
testCases := []struct {
Name string
Input string
HttpError bool
Valid bool
}{
{
Name: "namedIntrospectionQuery",
Input: namedIntrospectionQuery,
HttpError: true,
},
{
Name: "singleNamedIntrospectionQueryWithoutOperationName",
Input: singleNamedIntrospectionQueryWithoutOperationName,
HttpError: true,
},
{
Name: "silentIntrospectionQuery",
Input: silentIntrospectionQuery,
HttpError: true,
},
{
Name: "silentIntrospectionQueryWithOperationName",
Input: silentIntrospectionQueryWithOperationName,
HttpError: true,
},
{
Name: "schemaIntrospectionQueryWithMultipleQueries",
Input: schemaIntrospectionQueryWithMultipleQueries,
HttpError: true,
},
{
Name: "inlineFragmentedIntrospectionQueryType",
Input: inlineFragmentedIntrospectionQueryType,
HttpError: true,
},
{
Name: "inlineFragmentedIntrospectionQueryWithFragmentOnQuery",
Input: inlineFragmentedIntrospectionQueryWithFragmentOnQuery,
HttpError: true,
},
{
Name: "fragmentedIntrospectionQuery",
Input: fragmentedIntrospectionQuery,
HttpError: true,
},
{
Name: "typeIntrospectionQueryWithMultipleQueries",
Input: typeIntrospectionQueryWithMultipleQueries,
HttpError: true,
},
{
Name: "typeIntrospectionQuery",
Input: typeIntrospectionQuery,
HttpError: true,
},
{
Name: "nonIntrospectionQuery",
Input: nonIntrospectionQuery,
Valid: true,
},
{
Name: "nonIntrospectionQueryWithIntrospectionQueryName",
Input: nonIntrospectionQueryWithIntrospectionQueryName,
Valid: true,
},
{
Name: "nonSchemaIntrospectionQueryWithAliases",
Input: nonSchemaIntrospectionQueryWithAliases,
Valid: true,
},
{
Name: "nonTypeIntrospectionQueryWithAliases",
Input: nonTypeIntrospectionQueryWithAliases,
Valid: true,
},
{
Name: "nonSchemaIntrospectionQueryWithAdditionalFields",
Input: nonSchemaIntrospectionQueryWithAdditionalFields,
Valid: true,
},
{
Name: "nonTypeIntrospectionQueryWithAdditionalFields",
Input: nonTypeIntrospectionQueryWithAdditionalFields,
Valid: true,
},
{
Name: "nonSchemaIntrospectionQueryWithMultipleQueries",
Input: nonSchemaIntrospectionQueryWithMultipleQueries,
Valid: true,
},
{
Name: "nonTypeIntrospectionQueryWithMultipleQueries",
Input: nonTypeIntrospectionQueryWithMultipleQueries,
Valid: true,
},
{
Name: "mutationQuery",
Input: mutationQuery,
Valid: true,
},
}

var inputError HttpError
for _, tc := range testCases {
tc := tc
t.Run(tc.Name, func(t *testing.T) {

kit, err := parser.NewKit()
require.NoError(t, err)
defer kit.Free()

err = kit.UnmarshalOperationFromBody([]byte(tc.Input))
assert.NoError(t, err)

err = kit.Parse()

if tc.Valid {
assert.NoError(t, err)
} else if tc.HttpError {
assert.True(t, errors.As(err, &inputError), "expected an http error, got %s", err)
assert.Equal(t, err.Error(), "GraphQL introspection is disabled by Cosmo Router, but the query contained __schema or __type. To enable introspection, set introspection_enabled: true in the Router configuration")
} else {
assert.Error(t, err)
}
})
}
}

0 comments on commit 7d7a854

Please sign in to comment.