From 1f2b7bc0b30ae22199b9062fd69059e5d9c5b557 Mon Sep 17 00:00:00 2001 From: Luc Vauvillier Date: Thu, 2 Jul 2020 19:08:34 +0200 Subject: [PATCH 1/2] feat(server): disable graphql introspection in production --- src/runtime/server/handler-graphql.spec.ts | 3 ++ src/runtime/server/handler-graphql.ts | 43 ++++++++++++++++++++-- src/runtime/server/server.ts | 8 +++- src/runtime/server/settings.ts | 21 ++++++++++- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/runtime/server/handler-graphql.spec.ts b/src/runtime/server/handler-graphql.spec.ts index 425e11ee2..3014f98cc 100644 --- a/src/runtime/server/handler-graphql.spec.ts +++ b/src/runtime/server/handler-graphql.spec.ts @@ -162,6 +162,9 @@ function createHandler(...types: any) { }), () => { return {} + }, + { + introspection: true, } ) } diff --git a/src/runtime/server/handler-graphql.ts b/src/runtime/server/handler-graphql.ts index 9d342c45e..2950c7448 100644 --- a/src/runtime/server/handler-graphql.ts +++ b/src/runtime/server/handler-graphql.ts @@ -1,5 +1,15 @@ 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, + FieldNode, + GraphQLError, +} from 'graphql' import { IncomingMessage } from 'http' import createError, { HttpError } from 'http-errors' import url from 'url' @@ -7,7 +17,15 @@ 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 @@ -16,10 +34,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)) { @@ -42,7 +76,8 @@ export const createRequestHandlerGraphQL: CreateHandler = (schema, createContext const documentAST = errDocumentAST.right - const validationFailures = validate(schema, documentAST) + const validationRules = !settings.introspection ? [NoIntrospection] : [] + const validationFailures = validate(schema, documentAST, validationRules) if (validationFailures.length > 0) { // todo lots of rich info for clients in here, expose it to them diff --git a/src/runtime/server/server.ts b/src/runtime/server/server.ts index 094c178da..19feeda21 100644 --- a/src/runtime/server/server.ts +++ b/src/runtime/server/server.ts @@ -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 ) @@ -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)) diff --git a/src/runtime/server/settings.ts b/src/runtime/server/settings.ts index 859a7f9f9..bc885e4dc 100644 --- a/src/runtime/server/settings.ts +++ b/src/runtime/server/settings.ts @@ -8,6 +8,10 @@ export type PlaygroundSettings = { path?: string } +export type GraphqlSettings = { + introspection?: boolean +} + export type SettingsInput = { /** * todo @@ -50,11 +54,16 @@ export type SettingsInput = { path: string playgroundPath?: string }) => void + /** + * todo + */ + graphql?: GraphqlSettings } -export type SettingsData = Omit, 'host' | 'playground'> & { +export type SettingsData = Omit, 'host' | 'playground' | 'graphql'> & { host: string | undefined playground: false | Required + graphql: Required } export const defaultPlaygroundPath = '/' @@ -63,6 +72,10 @@ export const defaultPlaygroundSettings: () => Readonly Readonly> = () => ({ + 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. @@ -86,6 +99,7 @@ export const defaultSettings: () => Readonly = () => { }, playground: process.env.NODE_ENV === 'production' ? false : defaultPlaygroundSettings(), path: '/graphql', + graphql: defaultGraphqlSettings(), } } @@ -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 @@ -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() { From 3c4989f5ebb7c3f3ac9f36b2e2dfc0f082100d28 Mon Sep 17 00:00:00 2001 From: Luc Vauvillier Date: Thu, 2 Jul 2020 19:31:17 +0200 Subject: [PATCH 2/2] Fix missing default rules --- src/runtime/server/handler-graphql.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/runtime/server/handler-graphql.ts b/src/runtime/server/handler-graphql.ts index 2950c7448..303afd3fe 100644 --- a/src/runtime/server/handler-graphql.ts +++ b/src/runtime/server/handler-graphql.ts @@ -7,6 +7,7 @@ import { Source, validate, ValidationContext, + specifiedRules, FieldNode, GraphQLError, } from 'graphql' @@ -76,8 +77,11 @@ export const createRequestHandlerGraphQL: CreateHandler = (schema, createContext const documentAST = errDocumentAST.right - const validationRules = !settings.introspection ? [NoIntrospection] : [] - const validationFailures = validate(schema, documentAST, validationRules) + 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