fix: Data schema exposed via GraphQL API public introspection (GHSA-48q3-prgv-gm4w) (#9820)
This commit is contained in:
@@ -4,7 +4,7 @@ import { ApolloServer } from '@apollo/server';
|
||||
import { expressMiddleware } from '@apollo/server/express4';
|
||||
import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';
|
||||
import express from 'express';
|
||||
import { execute, subscribe } from 'graphql';
|
||||
import { execute, subscribe, GraphQLError } from 'graphql';
|
||||
import { SubscriptionServer } from 'subscriptions-transport-ws';
|
||||
import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
|
||||
import requiredParameter from '../requiredParameter';
|
||||
@@ -12,6 +12,45 @@ import defaultLogger from '../logger';
|
||||
import { ParseGraphQLSchema } from './ParseGraphQLSchema';
|
||||
import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController';
|
||||
|
||||
|
||||
const IntrospectionControlPlugin = (publicIntrospection) => ({
|
||||
|
||||
|
||||
requestDidStart: (requestContext) => ({
|
||||
|
||||
didResolveOperation: async () => {
|
||||
// If public introspection is enabled, we allow all introspection queries
|
||||
if (publicIntrospection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isMasterOrMaintenance = requestContext.contextValue.auth?.isMaster || requestContext.contextValue.auth?.isMaintenance
|
||||
if (isMasterOrMaintenance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Now we check if the query is an introspection query
|
||||
// this check strategy should work in 99.99% cases
|
||||
// we can have an issue if a user name a field or class __schemaSomething
|
||||
// we want to avoid a full AST check
|
||||
const isIntrospectionQuery =
|
||||
requestContext.request.query?.includes('__schema')
|
||||
|
||||
if (isIntrospectionQuery) {
|
||||
throw new GraphQLError('Introspection is not allowed', {
|
||||
extensions: {
|
||||
http: {
|
||||
status: 403,
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
class ParseGraphQLServer {
|
||||
parseGraphQLController: ParseGraphQLController;
|
||||
|
||||
@@ -65,8 +104,8 @@ class ParseGraphQLServer {
|
||||
// needed since we use graphql upload
|
||||
requestHeaders: ['X-Parse-Application-Id'],
|
||||
},
|
||||
introspection: true,
|
||||
plugins: [ApolloServerPluginCacheControlDisabled()],
|
||||
introspection: this.config.graphQLPublicIntrospection,
|
||||
plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)],
|
||||
schema,
|
||||
});
|
||||
await apollo.start();
|
||||
@@ -118,7 +157,7 @@ class ParseGraphQLServer {
|
||||
|
||||
app.get(
|
||||
this.config.playgroundPath ||
|
||||
requiredParameter('You must provide a config.playgroundPath to applyPlayground!'),
|
||||
requiredParameter('You must provide a config.playgroundPath to applyPlayground!'),
|
||||
(_req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.write(
|
||||
|
||||
@@ -292,6 +292,12 @@ module.exports.ParseServerOptions = {
|
||||
help: 'Mount path for the GraphQL endpoint, defaults to /graphql',
|
||||
default: '/graphql',
|
||||
},
|
||||
graphQLPublicIntrospection: {
|
||||
env: 'PARSE_SERVER_GRAPHQL_PUBLIC_INTROSPECTION',
|
||||
help: 'Enable public introspection for the GraphQL endpoint, defaults to false',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
graphQLSchema: {
|
||||
env: 'PARSE_SERVER_GRAPH_QLSCHEMA',
|
||||
help: 'Full path to your GraphQL custom schema.graphql file',
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
* @property {Adapter<FilesAdapter>} filesAdapter Adapter module for the files sub-system
|
||||
* @property {FileUploadOptions} fileUpload Options for file uploads
|
||||
* @property {String} graphQLPath Mount path for the GraphQL endpoint, defaults to /graphql
|
||||
* @property {Boolean} graphQLPublicIntrospection Enable public introspection for the GraphQL endpoint, defaults to false
|
||||
* @property {String} graphQLSchema Full path to your GraphQL custom schema.graphql file
|
||||
* @property {String} host The host to serve ParseServer on, defaults to 0.0.0.0
|
||||
* @property {IdempotencyOptions} idempotencyOptions Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.
|
||||
|
||||
@@ -304,6 +304,10 @@ export interface ParseServerOptions {
|
||||
:ENV: PARSE_SERVER_GRAPHQL_PATH
|
||||
:DEFAULT: /graphql */
|
||||
graphQLPath: ?string;
|
||||
/* Enable public introspection for the GraphQL endpoint, defaults to false
|
||||
:ENV: PARSE_SERVER_GRAPHQL_PUBLIC_INTROSPECTION
|
||||
:DEFAULT: false */
|
||||
graphQLPublicIntrospection: ?boolean;
|
||||
/* Mounts the GraphQL Playground - never use this option in production
|
||||
:ENV: PARSE_SERVER_MOUNT_PLAYGROUND
|
||||
:DEFAULT: false */
|
||||
|
||||
@@ -80,6 +80,16 @@ class CheckGroupServerConfig extends CheckGroup {
|
||||
}
|
||||
},
|
||||
}),
|
||||
new Check({
|
||||
title: 'GraphQL public introspection disabled',
|
||||
warning: 'GraphQL public introspection is enabled, which allows anyone to access the GraphQL schema.',
|
||||
solution: "Change Parse Server configuration to 'graphQLPublicIntrospection: false'. You will need to use master key or maintenance key to access the GraphQL schema.",
|
||||
check: () => {
|
||||
if (config.graphQLPublicIntrospection !== false) {
|
||||
throw 1;
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,9 +538,9 @@ export const addRateLimit = (route, config, cloud) => {
|
||||
url: route.redisUrl,
|
||||
});
|
||||
client.on('error', err => { log.error('Middlewares addRateLimit Redis client error', { error: err }) });
|
||||
client.on('connect', () => {});
|
||||
client.on('reconnecting', () => {});
|
||||
client.on('ready', () => {});
|
||||
client.on('connect', () => { });
|
||||
client.on('reconnecting', () => { });
|
||||
client.on('ready', () => { });
|
||||
redisStore.connectionPromise = async () => {
|
||||
if (client.isOpen) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user