Skip to content
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

feat(server): disable graphql introspection in production #1170

Merged
merged 2 commits into from
Jul 3, 2020
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
3 changes: 3 additions & 0 deletions src/runtime/server/handler-graphql.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ function createHandler(...types: any) {
}),
() => {
return {}
},
{
introspection: true,
}
)
}
Expand Down
47 changes: 43 additions & 4 deletions src/runtime/server/handler-graphql.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import { Either, isLeft, left, right, toError, tryCatch } from 'fp-ts/lib/Either'
import { execute, getOperationAST, GraphQLSchema, parse, Source, validate } from 'graphql'
import {
execute,
getOperationAST,
GraphQLSchema,
parse,
Source,
validate,
ValidationContext,
specifiedRules,
FieldNode,
GraphQLError,
} from 'graphql'
import { IncomingMessage } from 'http'
import createError, { HttpError } from 'http-errors'
import url from 'url'
import { parseBody } from './parse-body'
import { ContextCreator, NexusRequestHandler } from './server'
import { sendError, sendErrorData, sendSuccess } from './utils'

type CreateHandler = (schema: GraphQLSchema, createContext: ContextCreator) => NexusRequestHandler
type Settings = {
introspection: boolean
}

type CreateHandler = (
schema: GraphQLSchema,
createContext: ContextCreator,
settings: Settings
) => NexusRequestHandler

type GraphQLParams = {
query: null | string
Expand All @@ -16,10 +35,26 @@ type GraphQLParams = {
raw: boolean
}

const NoIntrospection = (context: ValidationContext) => ({
Field(node: FieldNode) {
if (node.name.value === '__schema' || node.name.value === '__type') {
context.reportError(
new GraphQLError(
'GraphQL introspection is not allowed by Nexus, but the query contained __schema or __type. To enable introspection, pass introspection: true to Nexus graphql settings in production',
[node]
)
)
}
},
})

/**
* Create a handler for graphql requests.
*/
export const createRequestHandlerGraphQL: CreateHandler = (schema, createContext) => async (req, res) => {
export const createRequestHandlerGraphQL: CreateHandler = (schema, createContext, settings) => async (
req,
res
) => {
const errParams = await getGraphQLParams(req)

if (isLeft(errParams)) {
Expand All @@ -42,7 +77,11 @@ export const createRequestHandlerGraphQL: CreateHandler = (schema, createContext

const documentAST = errDocumentAST.right

const validationFailures = validate(schema, documentAST)
let rules = specifiedRules
if (!settings.introspection) {
rules = [...rules, NoIntrospection]
}
const validationFailures = validate(schema, documentAST, rules)

if (validationFailures.length > 0) {
// todo lots of rich info for clients in here, expose it to them
Expand Down
8 changes: 6 additions & 2 deletions src/runtime/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ export function create(appState: AppState) {
return (
assembledGuard(appState, 'app.server.handlers.graphql', () => {
return wrapHandlerWithErrorHandling(
createRequestHandlerGraphQL(appState.assembled!.schema, appState.assembled!.createContext)
createRequestHandlerGraphQL(
appState.assembled!.schema,
appState.assembled!.createContext,
settings.data.graphql
)
)
}) ?? noop
)
Expand Down Expand Up @@ -106,7 +110,7 @@ export function create(appState: AppState) {
loadedRuntimePlugins
)

const graphqlHandler = createRequestHandlerGraphQL(schema, createContext)
const graphqlHandler = createRequestHandlerGraphQL(schema, createContext, settings.data.graphql)

express.post(settings.data.path, wrapHandlerWithErrorHandling(graphqlHandler))
express.get(settings.data.path, wrapHandlerWithErrorHandling(graphqlHandler))
Expand Down
21 changes: 20 additions & 1 deletion src/runtime/server/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export type PlaygroundSettings = {
path?: string
}

export type GraphqlSettings = {
introspection?: boolean
}

export type SettingsInput = {
/**
* todo
Expand Down Expand Up @@ -50,11 +54,16 @@ export type SettingsInput = {
path: string
playgroundPath?: string
}) => void
/**
* todo
*/
graphql?: GraphqlSettings
}

export type SettingsData = Omit<Utils.DeepRequired<SettingsInput>, 'host' | 'playground'> & {
export type SettingsData = Omit<Utils.DeepRequired<SettingsInput>, 'host' | 'playground' | 'graphql'> & {
host: string | undefined
playground: false | Required<PlaygroundSettings>
graphql: Required<GraphqlSettings>
}

export const defaultPlaygroundPath = '/'
Expand All @@ -63,6 +72,10 @@ export const defaultPlaygroundSettings: () => Readonly<Required<PlaygroundSettin
path: defaultPlaygroundPath,
})

export const defaultGraphqlSettings: () => Readonly<Required<GraphqlSettings>> = () => ({
introspection: process.env.NODE_ENV === 'production' ? false : true,
})

/**
* The default server options. These are merged with whatever you provide. Your
* settings take precedence over these.
Expand All @@ -86,6 +99,7 @@ export const defaultSettings: () => Readonly<SettingsData> = () => {
},
playground: process.env.NODE_ENV === 'production' ? false : defaultPlaygroundSettings(),
path: '/graphql',
graphql: defaultGraphqlSettings(),
}
}

Expand Down Expand Up @@ -121,6 +135,10 @@ export function playgroundSettings(settings: SettingsInput['playground']): Setti
}
}

export function graphqlSettings(settings: SettingsInput['graphql']): SettingsData['graphql'] {
return { ...defaultGraphqlSettings(), ...settings }
}

function validateGraphQLPath(path: string): string {
let outputPath = path

Expand All @@ -147,6 +165,7 @@ export function changeSettings(state: SettingsData, newSettings: SettingsInput):
state.path = validateGraphQLPath(updatedSettings.path)
state.port = updatedSettings.port
state.startMessage = updatedSettings.startMessage
state.graphql = graphqlSettings(updatedSettings.graphql)
}

export function createServerSettingsManager() {
Expand Down