mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
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:
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user