fix: GraphQL "Did you mean" validation suggestions disclose schema to unauthenticated callers ([GHSA-8cph-rgr4-g5vj](https://github.com/parse-community/parse-server/security/advisories/GHSA-8cph-rgr4-g5vj)) (#10467)

This commit is contained in:
Manuel
2026-05-18 17:04:56 +02:00
committed by GitHub
parent 216cf8335b
commit 155123ade9
2 changed files with 137 additions and 1 deletions
+109
View File
@@ -1016,6 +1016,115 @@ describe('ParseGraphQLServer', () => {
expect(introspection.data).toBeDefined();
expect(introspection.data.__type).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');
}
});
});
+28 -1
View File
@@ -90,6 +90,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;
@@ -153,7 +180,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), createComplexityValidationPlugin(() => this.parseServer.config.requestComplexity)],
plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection), SchemaSuggestionsControlPlugin(this.config.graphQLPublicIntrospection), createComplexityValidationPlugin(() => this.parseServer.config.requestComplexity)],
schema,
});
await apollo.start();