Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,132 @@ describe('ParseGraphQLServer', () => {
expect(error.message).toContain('secretAdminTask');
}
});

it('should strip required-field names from base coercion errors without master or maintenance key', async () => {
const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('TestReqClass', {
secretRequiredField: { type: 'String', required: true },
});
await resetGraphQLCache();

try {
await apolloClient.mutate({
mutation: gql`
mutation Create($input: CreateTestReqClassInput!) {
createTestReqClass(input: $input) {
testReqClass {
id
}
}
}
`,
variables: { input: { fields: {} } },
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
// The base graphql-js "... was not provided." coercion message carries no
// "Did you mean" clause, so it discloses the required custom field name to a
// caller who only has the public application id. It must be redacted.
expect(error.message).not.toContain('secretRequiredField');
// The message is duplicated into extensions.stacktrace in non-production;
// ensure the identifier does not leak through any returned field.
expect(JSON.stringify(error)).not.toContain('secretRequiredField');
}
});

it('should keep required-field names in base coercion errors with master key', async () => {
const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('TestReqClass', {
secretRequiredField: { type: 'String', required: true },
});
await resetGraphQLCache();

try {
await apolloClient.mutate({
mutation: gql`
mutation Create($input: CreateTestReqClassInput!) {
createTestReqClass(input: $input) {
testReqClass {
id
}
}
}
`,
variables: { input: { fields: {} } },
context: {
headers: {
'X-Parse-Master-Key': 'test',
},
},
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).toContain('secretRequiredField');
}
});

it('should keep required-field names in base coercion errors when public introspection is enabled', async () => {
const parseServer = await reconfigureServer();
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('TestReqClass', {
secretRequiredField: { type: 'String', required: true },
});
await resetGraphQLCache();

try {
await apolloClient.mutate({
mutation: gql`
mutation Create($input: CreateTestReqClassInput!) {
createTestReqClass(input: $input) {
testReqClass {
id
}
}
}
`,
variables: { input: { fields: {} } },
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).toContain('secretRequiredField');
}
});

it('should strip required-field names from inline-literal coercion errors without master or maintenance key', async () => {
const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('TestReqClass', {
secretRequiredField: { type: 'String', required: true },
});
await resetGraphQLCache();

try {
// Input written inline in the operation (not via a variable) is validated by
// ValuesOfCorrectTypeRule, which emits a type-qualified message
// ('Field "<Type>.<field>" of required type ...'), disclosing both the generated
// input type name (which embeds the class name) and the required field name.
await apolloClient.mutate({
mutation: gql`
mutation Create {
createTestReqClass(input: { fields: {} }) {
testReqClass {
id
}
}
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).not.toContain('secretRequiredField');
expect(error.message).not.toContain('CreateTestReqClass');
expect(JSON.stringify(error)).not.toContain('secretRequiredField');
}
});
});


Expand Down
25 changes: 23 additions & 2 deletions src/GraphQL/ParseGraphQLServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,27 @@ const IntrospectionControlPlugin = (publicIntrospection) => ({
const stripSchemaSuggestion = message =>
typeof message === 'string' ? message.replace(/ ?Did you mean(.+?)\?$/, '') : message;

// graphql-js also emits a base input-coercion message that names a schema
// identifier WITHOUT a "Did you mean" clause, so the suggestion strip above
// cannot reach it: when a required custom input field is omitted, coerceInputValue
// returns 'Field "<name>" of required type "<type>" was not provided.', disclosing
// a field name the caller never supplied. Redact the quoted identifiers from this
// template while preserving the error shape, for callers that are not allowed to
// introspect. The sibling coercion messages ('... is not defined by type "<type>".',
// 'Expected type "<type>" to be an object.') are intentionally left intact: they
// only echo an input type name the caller already referenced in the operation, so
// they disclose nothing the caller did not already provide.
const stripSchemaCoercionIdentifiers = message =>
typeof message === 'string'
? message.replace(
/Field "[^"]*" of required type "[^"]*" was not provided\./g,
'Field of required type was not provided.'
)
: message;

const stripSchemaIdentifiers = message =>
stripSchemaCoercionIdentifiers(stripSchemaSuggestion(message));

const SchemaSuggestionsControlPlugin = (publicIntrospection) => ({
requestDidStart: async (requestContext) => ({
willSendResponse: async () => {
Expand All @@ -124,9 +145,9 @@ const SchemaSuggestionsControlPlugin = (publicIntrospection) => ({
? body.initialResult.errors
: undefined;
errors?.forEach(error => {
error.message = stripSchemaSuggestion(error.message);
error.message = stripSchemaIdentifiers(error.message);
if (Array.isArray(error.extensions?.stacktrace)) {
error.extensions.stacktrace = error.extensions.stacktrace.map(stripSchemaSuggestion);
error.extensions.stacktrace = error.extensions.stacktrace.map(stripSchemaIdentifiers);
}
});
},
Expand Down
Loading