From c96eea553f40df38d2032c92817f370f4f9a2384 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:42:52 +0200 Subject: [PATCH] fix: Parse Server option graphQLPublicIntrospection has no effect --- spec/ParseGraphQLServer.spec.js | 93 +++++++++++++++++++++++++++++++ src/GraphQL/ParseGraphQLServer.js | 7 ++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 05c1df683d..3e3ac6aaad 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -761,6 +761,99 @@ describe('ParseGraphQLServer', () => { expect(introspection.data).toBeDefined(); }); + it('should have public introspection enabled if enabled via the Parse Server option', async () => { + const parseServer = await reconfigureServer({ graphQLPublicIntrospection: true }); + await createGQLFromParseServer(parseServer); + + const introspection = await apolloClient.query({ + query: gql` + query Introspection { + __schema { + types { + name + } + } + } + `, + }); + expect(introspection.data.__schema).toBeDefined(); + }); + + it('should keep "Did you mean" suggestions when public introspection is enabled via the Parse Server option', async () => { + const parseServer = await reconfigureServer({ graphQLPublicIntrospection: true }); + await createGQLFromParseServer(parseServer); + + try { + await apolloClient.query({ + query: gql` + query Typo { + healt + } + `, + }); + fail('should have thrown a validation error'); + } catch (e) { + const message = e.networkError.result.errors[0].message; + expect(message).toContain('Cannot query field "healt"'); + expect(message).toMatch(/Did you mean/); + expect(message).toContain('health'); + } + }); + + it('should prefer the GraphQL server option over the Parse Server option for public introspection', async () => { + const parseServer = await reconfigureServer({ graphQLPublicIntrospection: true }); + await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: false }); + + 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'); + } + }); + + describe('mounted via Parse Server option mountGraphQL', () => { + const introspectionRequest = async () => { + const res = await fetch('http://localhost:8378/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + }, + body: JSON.stringify({ query: '{ __schema { types { name } } }' }), + }); + return { status: res.status, body: JSON.parse(await res.text()) }; + }; + + it('should have public introspection disabled by default without master key', async () => { + await reconfigureServer({ mountGraphQL: true }); + const response = await introspectionRequest(); + expect(response.status).toEqual(403); + expect(response.body.data).toBeUndefined(); + expect(response.body.errors[0].message).toEqual('Introspection is not allowed'); + }); + + it('should have public introspection enabled if enabled', async () => { + await reconfigureServer({ mountGraphQL: true, graphQLPublicIntrospection: true }); + const response = await introspectionRequest(); + expect(response.status).toEqual(200); + expect(response.body.errors).toBeUndefined(); + expect(response.body.data.__schema).toBeDefined(); + }); + }); + it('should block __type introspection without master key', async () => { try { await apolloClient.query({ diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 3551d04ac8..9d4974468d 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -348,6 +348,11 @@ class ParseGraphQLServer { const createServer = async () => { try { const { schema, context } = await this._getGraphQLOptions(); + // A value passed to this GraphQL server takes precedence; otherwise use the Parse Server + // option, where the option is documented and where the security check reads it. + const publicIntrospection = + this.config.graphQLPublicIntrospection ?? + this.parseServer.config.graphQLPublicIntrospection; const apollo = new ApolloServer({ csrfPrevention: { // See https://www.apollographql.com/docs/router/configuration/csrf/ @@ -357,7 +362,7 @@ class ParseGraphQLServer { // We need always true introspection because apollo server have changing behavior based on the NODE_ENV variable // we delegate the introspection control to the IntrospectionControlPlugin introspection: true, - plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection), SchemaSuggestionsControlPlugin(this.config.graphQLPublicIntrospection), createComplexityValidationPlugin(() => this.parseServer.config.requestComplexity)], + plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(publicIntrospection), SchemaSuggestionsControlPlugin(publicIntrospection), createComplexityValidationPlugin(() => this.parseServer.config.requestComplexity)], schema, }); await apollo.start();