From 5d7132dfe648ec0a2717847d7d9ee4760aa49009 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 18 May 2026 03:17:33 +0200 Subject: [PATCH] fix: GHSA-8cph-rgr4-g5vj --- spec/ParseGraphQLServer.spec.js | 109 ++++++++++++++++++++++++++++++ src/GraphQL/ParseGraphQLServer.js | 29 +++++++- 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 045efd22f2..12651a0061 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -715,6 +715,115 @@ describe('ParseGraphQLServer', () => { }) expect(introspection.data).toBeDefined(); }); + + it('should strip "Did you mean" field suggestions from validation errors without master or maintenance key', async () => { + 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).not.toMatch(/Did you mean/); + expect(message).not.toContain('health'); + } + }); + + it('should strip "Did you mean" argument suggestions from validation errors without master or maintenance key', async () => { + try { + await apolloClient.query({ + query: gql` + query UnknownArg { + users(wher: {}) { + edges { + node { + id + } + } + } + } + `, + }); + fail('should have thrown a validation error'); + } catch (e) { + const message = e.networkError.result.errors[0].message; + expect(message).toContain('Unknown argument "wher"'); + expect(message).not.toMatch(/Did you mean/); + expect(message).not.toContain('"where"'); + } + }); + + it('should keep "Did you mean" suggestions with master key', async () => { + try { + await apolloClient.query({ + query: gql` + query Typo { + healt + } + `, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + 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 keep "Did you mean" suggestions with maintenance key', async () => { + try { + await apolloClient.query({ + query: gql` + query Typo { + healt + } + `, + context: { + headers: { + 'X-Parse-Maintenance-Key': 'test2', + }, + }, + }); + 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 keep "Did you mean" suggestions when public introspection is enabled', async () => { + const parseServer = await reconfigureServer(); + await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true }); + + 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'); + } + }); }); diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 56daddb798..0b10f57b2c 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -50,6 +50,33 @@ const IntrospectionControlPlugin = (publicIntrospection) => ({ }); +// graphql-js validation rules (FieldsOnCorrectTypeRule, KnownArgumentNamesRule, +// KnownTypeNamesRule, ...) embed "Did you mean ...?" hints sourced from the live +// schema in their error messages. Those messages are returned to the caller +// before didResolveOperation runs, so they sidestep IntrospectionControlPlugin +// and disclose schema identifiers the introspection guard is meant to hide. +// Strip the hint suffix for callers that are not allowed to introspect. +const SchemaSuggestionsControlPlugin = (publicIntrospection) => ({ + requestDidStart: async (requestContext) => ({ + validationDidStart: async () => { + if (publicIntrospection) { + return; + } + const isMasterOrMaintenance = + requestContext.contextValue.auth?.isMaster || + requestContext.contextValue.auth?.isMaintenance; + if (isMasterOrMaintenance) { + return; + } + return async (validationErrors) => { + validationErrors?.forEach(error => { + error.message = error.message.replace(/ ?Did you mean(.+?)\?$/, ''); + }); + }; + }, + }), +}); + class ParseGraphQLServer { parseGraphQLController: ParseGraphQLController; @@ -111,7 +138,7 @@ class ParseGraphQLServer { requestHeaders: ['X-Parse-Application-Id'], }, introspection: this.config.graphQLPublicIntrospection, - plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection), createComplexityValidationPlugin(() => this.parseServer.config.requestComplexity)], + plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection), SchemaSuggestionsControlPlugin(this.config.graphQLPublicIntrospection), createComplexityValidationPlugin(() => this.parseServer.config.requestComplexity)], schema, }); await apollo.start();