Skip to content

Commit 155123a

Browse files
authored
fix: GraphQL "Did you mean" validation suggestions disclose schema to unauthenticated callers ([GHSA-8cph-rgr4-g5vj](GHSA-8cph-rgr4-g5vj)) (#10467)
1 parent 216cf83 commit 155123a

2 files changed

Lines changed: 137 additions & 1 deletion

File tree

spec/ParseGraphQLServer.spec.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,115 @@ describe('ParseGraphQLServer', () => {
10161016
expect(introspection.data).toBeDefined();
10171017
expect(introspection.data.__type).toBeDefined();
10181018
});
1019+
1020+
it('should strip "Did you mean" field suggestions from validation errors without master or maintenance key', async () => {
1021+
try {
1022+
await apolloClient.query({
1023+
query: gql`
1024+
query Typo {
1025+
healt
1026+
}
1027+
`,
1028+
});
1029+
fail('should have thrown a validation error');
1030+
} catch (e) {
1031+
const message = e.networkError.result.errors[0].message;
1032+
expect(message).toContain('Cannot query field "healt"');
1033+
expect(message).not.toMatch(/Did you mean/);
1034+
expect(message).not.toContain('health');
1035+
}
1036+
});
1037+
1038+
it('should strip "Did you mean" argument suggestions from validation errors without master or maintenance key', async () => {
1039+
try {
1040+
await apolloClient.query({
1041+
query: gql`
1042+
query UnknownArg {
1043+
users(wher: {}) {
1044+
edges {
1045+
node {
1046+
id
1047+
}
1048+
}
1049+
}
1050+
}
1051+
`,
1052+
});
1053+
fail('should have thrown a validation error');
1054+
} catch (e) {
1055+
const message = e.networkError.result.errors[0].message;
1056+
expect(message).toContain('Unknown argument "wher"');
1057+
expect(message).not.toMatch(/Did you mean/);
1058+
expect(message).not.toContain('"where"');
1059+
}
1060+
});
1061+
1062+
it('should keep "Did you mean" suggestions with master key', async () => {
1063+
try {
1064+
await apolloClient.query({
1065+
query: gql`
1066+
query Typo {
1067+
healt
1068+
}
1069+
`,
1070+
context: {
1071+
headers: {
1072+
'X-Parse-Master-Key': 'test',
1073+
},
1074+
},
1075+
});
1076+
fail('should have thrown a validation error');
1077+
} catch (e) {
1078+
const message = e.networkError.result.errors[0].message;
1079+
expect(message).toContain('Cannot query field "healt"');
1080+
expect(message).toMatch(/Did you mean/);
1081+
expect(message).toContain('health');
1082+
}
1083+
});
1084+
1085+
it('should keep "Did you mean" suggestions with maintenance key', async () => {
1086+
try {
1087+
await apolloClient.query({
1088+
query: gql`
1089+
query Typo {
1090+
healt
1091+
}
1092+
`,
1093+
context: {
1094+
headers: {
1095+
'X-Parse-Maintenance-Key': 'test2',
1096+
},
1097+
},
1098+
});
1099+
fail('should have thrown a validation error');
1100+
} catch (e) {
1101+
const message = e.networkError.result.errors[0].message;
1102+
expect(message).toContain('Cannot query field "healt"');
1103+
expect(message).toMatch(/Did you mean/);
1104+
expect(message).toContain('health');
1105+
}
1106+
});
1107+
1108+
it('should keep "Did you mean" suggestions when public introspection is enabled', async () => {
1109+
const parseServer = await reconfigureServer();
1110+
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
1111+
1112+
try {
1113+
await apolloClient.query({
1114+
query: gql`
1115+
query Typo {
1116+
healt
1117+
}
1118+
`,
1119+
});
1120+
fail('should have thrown a validation error');
1121+
} catch (e) {
1122+
const message = e.networkError.result.errors[0].message;
1123+
expect(message).toContain('Cannot query field "healt"');
1124+
expect(message).toMatch(/Did you mean/);
1125+
expect(message).toContain('health');
1126+
}
1127+
});
10191128
});
10201129

10211130

src/GraphQL/ParseGraphQLServer.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,33 @@ const IntrospectionControlPlugin = (publicIntrospection) => ({
9090

9191
});
9292

93+
// graphql-js validation rules (FieldsOnCorrectTypeRule, KnownArgumentNamesRule,
94+
// KnownTypeNamesRule, ...) embed "Did you mean ...?" hints sourced from the live
95+
// schema in their error messages. Those messages are returned to the caller
96+
// before didResolveOperation runs, so they sidestep IntrospectionControlPlugin
97+
// and disclose schema identifiers the introspection guard is meant to hide.
98+
// Strip the hint suffix for callers that are not allowed to introspect.
99+
const SchemaSuggestionsControlPlugin = (publicIntrospection) => ({
100+
requestDidStart: async (requestContext) => ({
101+
validationDidStart: async () => {
102+
if (publicIntrospection) {
103+
return;
104+
}
105+
const isMasterOrMaintenance =
106+
requestContext.contextValue.auth?.isMaster ||
107+
requestContext.contextValue.auth?.isMaintenance;
108+
if (isMasterOrMaintenance) {
109+
return;
110+
}
111+
return async (validationErrors) => {
112+
validationErrors?.forEach(error => {
113+
error.message = error.message.replace(/ ?Did you mean(.+?)\?$/, '');
114+
});
115+
};
116+
},
117+
}),
118+
});
119+
93120
class ParseGraphQLServer {
94121
parseGraphQLController: ParseGraphQLController;
95122

@@ -153,7 +180,7 @@ class ParseGraphQLServer {
153180
// We need always true introspection because apollo server have changing behavior based on the NODE_ENV variable
154181
// we delegate the introspection control to the IntrospectionControlPlugin
155182
introspection: true,
156-
plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection), createComplexityValidationPlugin(() => this.parseServer.config.requestComplexity)],
183+
plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection), SchemaSuggestionsControlPlugin(this.config.graphQLPublicIntrospection), createComplexityValidationPlugin(() => this.parseServer.config.requestComplexity)],
157184
schema,
158185
});
159186
await apollo.start();

0 commit comments

Comments
 (0)