fix: Data schema exposed via GraphQL API public introspection (GHSA-48q3-prgv-gm4w) (#9820)

This commit is contained in:
Manuel
2025-07-10 04:24:58 +02:00
committed by GitHub
parent 0f2aa28381
commit c10f4ac97b
8 changed files with 208 additions and 22 deletions

View File

@@ -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(

View File

@@ -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',

View 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.

View File

@@ -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 */

View File

@@ -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;
}
},
}),
];
}
}

View File

@@ -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;