fix: Data schema exposed via GraphQL API public introspection (GHSA-48q3-prgv-gm4w) (#9820)
This commit is contained in:
@@ -50,6 +50,7 @@ describe('ParseGraphQLServer', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
parseServer = await global.reconfigureServer({
|
parseServer = await global.reconfigureServer({
|
||||||
|
maintenanceKey: 'test2',
|
||||||
maxUploadSize: '1kb',
|
maxUploadSize: '1kb',
|
||||||
});
|
});
|
||||||
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
|
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
|
||||||
@@ -431,17 +432,33 @@ describe('ParseGraphQLServer', () => {
|
|||||||
objects.push(object1, object2, object3, object4);
|
objects.push(object1, object2, object3, object4);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
async function createGQLFromParseServer(_parseServer, parseGraphQLServerOptions) {
|
||||||
|
if (parseLiveQueryServer) {
|
||||||
|
await parseLiveQueryServer.server.close();
|
||||||
|
}
|
||||||
|
if (httpServer) {
|
||||||
|
await httpServer.close();
|
||||||
|
}
|
||||||
const expressApp = express();
|
const expressApp = express();
|
||||||
httpServer = http.createServer(expressApp);
|
httpServer = http.createServer(expressApp);
|
||||||
expressApp.use('/parse', parseServer.app);
|
expressApp.use('/parse', parseServer.app);
|
||||||
parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, {
|
parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, {
|
||||||
port: 1338,
|
port: 1338,
|
||||||
});
|
});
|
||||||
|
parseGraphQLServer = new ParseGraphQLServer(_parseServer, {
|
||||||
|
graphQLPath: '/graphql',
|
||||||
|
playgroundPath: '/playground',
|
||||||
|
subscriptionsPath: '/subscriptions',
|
||||||
|
...parseGraphQLServerOptions,
|
||||||
|
});
|
||||||
parseGraphQLServer.applyGraphQL(expressApp);
|
parseGraphQLServer.applyGraphQL(expressApp);
|
||||||
parseGraphQLServer.applyPlayground(expressApp);
|
parseGraphQLServer.applyPlayground(expressApp);
|
||||||
parseGraphQLServer.createSubscriptions(httpServer);
|
parseGraphQLServer.createSubscriptions(httpServer);
|
||||||
await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve));
|
await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createGQLFromParseServer(parseServer);
|
||||||
|
|
||||||
const subscriptionClient = new SubscriptionClient(
|
const subscriptionClient = new SubscriptionClient(
|
||||||
'ws://localhost:13377/subscriptions',
|
'ws://localhost:13377/subscriptions',
|
||||||
@@ -590,6 +607,96 @@ describe('ParseGraphQLServer', () => {
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
describe('Introspection', () => {
|
||||||
|
it('should have public introspection disabled by default without master key', async () => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apolloClient.query({
|
||||||
|
query: gql`
|
||||||
|
query Introspection {
|
||||||
|
__schema {
|
||||||
|
types {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
fail('should have thrown an error');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toEqual('Response not successful: Received status code 403');
|
||||||
|
expect(e.networkError.result.errors[0].message).toEqual('Introspection is not allowed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should always work with master key', async () => {
|
||||||
|
const introspection =
|
||||||
|
await apolloClient.query({
|
||||||
|
query: gql`
|
||||||
|
query Introspection {
|
||||||
|
__schema {
|
||||||
|
types {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},)
|
||||||
|
expect(introspection.data).toBeDefined();
|
||||||
|
expect(introspection.errors).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should always work with maintenance key', async () => {
|
||||||
|
const introspection =
|
||||||
|
await apolloClient.query({
|
||||||
|
query: gql`
|
||||||
|
query Introspection {
|
||||||
|
__schema {
|
||||||
|
types {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Maintenance-Key': 'test2',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},)
|
||||||
|
expect(introspection.data).toBeDefined();
|
||||||
|
expect(introspection.errors).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have public introspection enabled if enabled', async () => {
|
||||||
|
|
||||||
|
const parseServer = await reconfigureServer();
|
||||||
|
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
|
||||||
|
|
||||||
|
const introspection =
|
||||||
|
await apolloClient.query({
|
||||||
|
query: gql`
|
||||||
|
query Introspection {
|
||||||
|
__schema {
|
||||||
|
types {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
expect(introspection.data).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('Default Types', () => {
|
describe('Default Types', () => {
|
||||||
it('should have Object scalar type', async () => {
|
it('should have Object scalar type', async () => {
|
||||||
const objectType = (
|
const objectType = (
|
||||||
@@ -734,6 +841,11 @@ describe('ParseGraphQLServer', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
).data['__schema'].types.map(type => type.name);
|
).data['__schema'].types.map(type => type.name);
|
||||||
|
|
||||||
@@ -769,6 +881,11 @@ describe('ParseGraphQLServer', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
).data['__schema'].types.map(type => type.name);
|
).data['__schema'].types.map(type => type.name);
|
||||||
|
|
||||||
@@ -1301,6 +1418,11 @@ describe('ParseGraphQLServer', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
).data['__schema'].types.map(type => type.name);
|
).data['__schema'].types.map(type => type.name);
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ describe('Security Check Groups', () => {
|
|||||||
config.security.enableCheckLog = false;
|
config.security.enableCheckLog = false;
|
||||||
config.allowClientClassCreation = false;
|
config.allowClientClassCreation = false;
|
||||||
config.enableInsecureAuthAdapters = false;
|
config.enableInsecureAuthAdapters = false;
|
||||||
|
config.graphQLPublicIntrospection = false;
|
||||||
await reconfigureServer(config);
|
await reconfigureServer(config);
|
||||||
|
|
||||||
const group = new CheckGroupServerConfig();
|
const group = new CheckGroupServerConfig();
|
||||||
@@ -41,12 +42,14 @@ describe('Security Check Groups', () => {
|
|||||||
expect(group.checks()[1].checkState()).toBe(CheckState.success);
|
expect(group.checks()[1].checkState()).toBe(CheckState.success);
|
||||||
expect(group.checks()[2].checkState()).toBe(CheckState.success);
|
expect(group.checks()[2].checkState()).toBe(CheckState.success);
|
||||||
expect(group.checks()[4].checkState()).toBe(CheckState.success);
|
expect(group.checks()[4].checkState()).toBe(CheckState.success);
|
||||||
|
expect(group.checks()[5].checkState()).toBe(CheckState.success);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks fail correctly', async () => {
|
it('checks fail correctly', async () => {
|
||||||
config.masterKey = 'insecure';
|
config.masterKey = 'insecure';
|
||||||
config.security.enableCheckLog = true;
|
config.security.enableCheckLog = true;
|
||||||
config.allowClientClassCreation = true;
|
config.allowClientClassCreation = true;
|
||||||
|
config.graphQLPublicIntrospection = true;
|
||||||
await reconfigureServer(config);
|
await reconfigureServer(config);
|
||||||
|
|
||||||
const group = new CheckGroupServerConfig();
|
const group = new CheckGroupServerConfig();
|
||||||
@@ -55,6 +58,7 @@ describe('Security Check Groups', () => {
|
|||||||
expect(group.checks()[1].checkState()).toBe(CheckState.fail);
|
expect(group.checks()[1].checkState()).toBe(CheckState.fail);
|
||||||
expect(group.checks()[2].checkState()).toBe(CheckState.fail);
|
expect(group.checks()[2].checkState()).toBe(CheckState.fail);
|
||||||
expect(group.checks()[4].checkState()).toBe(CheckState.fail);
|
expect(group.checks()[4].checkState()).toBe(CheckState.fail);
|
||||||
|
expect(group.checks()[5].checkState()).toBe(CheckState.fail);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ApolloServer } from '@apollo/server';
|
|||||||
import { expressMiddleware } from '@apollo/server/express4';
|
import { expressMiddleware } from '@apollo/server/express4';
|
||||||
import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';
|
import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { execute, subscribe } from 'graphql';
|
import { execute, subscribe, GraphQLError } from 'graphql';
|
||||||
import { SubscriptionServer } from 'subscriptions-transport-ws';
|
import { SubscriptionServer } from 'subscriptions-transport-ws';
|
||||||
import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
|
import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
|
||||||
import requiredParameter from '../requiredParameter';
|
import requiredParameter from '../requiredParameter';
|
||||||
@@ -12,6 +12,45 @@ import defaultLogger from '../logger';
|
|||||||
import { ParseGraphQLSchema } from './ParseGraphQLSchema';
|
import { ParseGraphQLSchema } from './ParseGraphQLSchema';
|
||||||
import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController';
|
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 {
|
class ParseGraphQLServer {
|
||||||
parseGraphQLController: ParseGraphQLController;
|
parseGraphQLController: ParseGraphQLController;
|
||||||
|
|
||||||
@@ -65,8 +104,8 @@ class ParseGraphQLServer {
|
|||||||
// needed since we use graphql upload
|
// needed since we use graphql upload
|
||||||
requestHeaders: ['X-Parse-Application-Id'],
|
requestHeaders: ['X-Parse-Application-Id'],
|
||||||
},
|
},
|
||||||
introspection: true,
|
introspection: this.config.graphQLPublicIntrospection,
|
||||||
plugins: [ApolloServerPluginCacheControlDisabled()],
|
plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)],
|
||||||
schema,
|
schema,
|
||||||
});
|
});
|
||||||
await apollo.start();
|
await apollo.start();
|
||||||
|
|||||||
@@ -292,6 +292,12 @@ module.exports.ParseServerOptions = {
|
|||||||
help: 'Mount path for the GraphQL endpoint, defaults to /graphql',
|
help: 'Mount path for the GraphQL endpoint, defaults to /graphql',
|
||||||
default: '/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: {
|
graphQLSchema: {
|
||||||
env: 'PARSE_SERVER_GRAPH_QLSCHEMA',
|
env: 'PARSE_SERVER_GRAPH_QLSCHEMA',
|
||||||
help: 'Full path to your GraphQL custom schema.graphql file',
|
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 {Adapter<FilesAdapter>} filesAdapter Adapter module for the files sub-system
|
||||||
* @property {FileUploadOptions} fileUpload Options for file uploads
|
* @property {FileUploadOptions} fileUpload Options for file uploads
|
||||||
* @property {String} graphQLPath Mount path for the GraphQL endpoint, defaults to /graphql
|
* @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} 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 {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.
|
* @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
|
:ENV: PARSE_SERVER_GRAPHQL_PATH
|
||||||
:DEFAULT: /graphql */
|
:DEFAULT: /graphql */
|
||||||
graphQLPath: ?string;
|
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
|
/* Mounts the GraphQL Playground - never use this option in production
|
||||||
:ENV: PARSE_SERVER_MOUNT_PLAYGROUND
|
:ENV: PARSE_SERVER_MOUNT_PLAYGROUND
|
||||||
:DEFAULT: false */
|
: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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user