Skip to content

Commit 20e074a

Browse files
committed
feat: smart __type check
1 parent cbdd905 commit 20e074a

File tree

2 files changed

+201
-35
lines changed

2 files changed

+201
-35
lines changed

spec/ParseGraphQLServer.spec.js

Lines changed: 154 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -729,10 +729,134 @@ describe('ParseGraphQLServer', () => {
729729
})
730730
expect(introspection.data).toBeDefined();
731731
});
732+
733+
it('should block __type introspection without master key', async () => {
734+
try {
735+
await apolloClient.query({
736+
query: gql`
737+
query TypeIntrospection {
738+
__type(name: "User") {
739+
name
740+
kind
741+
}
742+
}
743+
`,
744+
});
745+
746+
fail('should have thrown an error');
747+
} catch (e) {
748+
expect(e.message).toEqual('Response not successful: Received status code 403');
749+
expect(e.networkError.result.errors[0].message).toEqual('Introspection is not allowed');
750+
}
751+
});
752+
753+
it('should block aliased __type introspection without master key', async () => {
754+
try {
755+
await apolloClient.query({
756+
query: gql`
757+
query AliasedTypeIntrospection {
758+
myAlias: __type(name: "User") {
759+
name
760+
kind
761+
}
762+
}
763+
`,
764+
});
765+
766+
fail('should have thrown an error');
767+
} catch (e) {
768+
expect(e.message).toEqual('Response not successful: Received status code 403');
769+
expect(e.networkError.result.errors[0].message).toEqual('Introspection is not allowed');
770+
}
771+
});
772+
773+
it('should allow __type introspection with master key', async () => {
774+
const introspection = await apolloClient.query({
775+
query: gql`
776+
query TypeIntrospection {
777+
__type(name: "User") {
778+
name
779+
kind
780+
}
781+
}
782+
`,
783+
context: {
784+
headers: {
785+
'X-Parse-Master-Key': 'test',
786+
},
787+
},
788+
});
789+
expect(introspection.data).toBeDefined();
790+
expect(introspection.data.__type).toBeDefined();
791+
expect(introspection.errors).not.toBeDefined();
792+
});
793+
794+
it('should allow aliased __type introspection with master key', async () => {
795+
const introspection = await apolloClient.query({
796+
query: gql`
797+
query AliasedTypeIntrospection {
798+
myAlias: __type(name: "User") {
799+
name
800+
kind
801+
}
802+
}
803+
`,
804+
context: {
805+
headers: {
806+
'X-Parse-Master-Key': 'test',
807+
},
808+
},
809+
});
810+
expect(introspection.data).toBeDefined();
811+
expect(introspection.data.myAlias).toBeDefined();
812+
expect(introspection.errors).not.toBeDefined();
813+
});
814+
815+
it('should allow __type introspection with maintenance key', async () => {
816+
const introspection = await apolloClient.query({
817+
query: gql`
818+
query TypeIntrospection {
819+
__type(name: "User") {
820+
name
821+
kind
822+
}
823+
}
824+
`,
825+
context: {
826+
headers: {
827+
'X-Parse-Maintenance-Key': 'test2',
828+
},
829+
},
830+
});
831+
expect(introspection.data).toBeDefined();
832+
expect(introspection.data.__type).toBeDefined();
833+
expect(introspection.errors).not.toBeDefined();
834+
});
835+
836+
it('should allow __type introspection when public introspection is enabled', async () => {
837+
const parseServer = await reconfigureServer();
838+
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
839+
840+
const introspection = await apolloClient.query({
841+
query: gql`
842+
query TypeIntrospection {
843+
__type(name: "User") {
844+
name
845+
kind
846+
}
847+
}
848+
`,
849+
});
850+
expect(introspection.data).toBeDefined();
851+
expect(introspection.data.__type).toBeDefined();
852+
});
732853
});
733854

734855

735856
describe('Default Types', () => {
857+
beforeEach(async () => {
858+
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
859+
});
736860
it('should have Object scalar type', async () => {
737861
const objectType = (
738862
await apolloClient.query({
@@ -892,6 +1016,10 @@ describe('ParseGraphQLServer', () => {
8921016
});
8931017

8941018
describe('Relay Specific Types', () => {
1019+
beforeEach(async () => {
1020+
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
1021+
});
1022+
8951023
let clearCache;
8961024
beforeEach(async () => {
8971025
if (!clearCache) {
@@ -1435,6 +1563,9 @@ describe('ParseGraphQLServer', () => {
14351563
});
14361564

14371565
describe('Parse Class Types', () => {
1566+
beforeEach(async () => {
1567+
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
1568+
});
14381569
it('should have all expected types', async () => {
14391570
await parseServer.config.databaseController.loadSchema();
14401571

@@ -1546,6 +1677,7 @@ describe('ParseGraphQLServer', () => {
15461677
beforeEach(async () => {
15471678
await parseGraphQLServer.setGraphQLConfig({});
15481679
await resetGraphQLCache();
1680+
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
15491681
});
15501682

15511683
it_id('d6a23a2f-ca18-4b15-bc73-3e636f99e6bc')(it)('should only include types in the enabledForClasses list', async () => {
@@ -6695,7 +6827,7 @@ describe('ParseGraphQLServer', () => {
66956827
);
66966828
expect(
66976829
(await deleteObject(object4.className, object4.id)).data.delete[
6698-
object4.className.charAt(0).toLowerCase() + object4.className.slice(1)
6830+
object4.className.charAt(0).toLowerCase() + object4.className.slice(1)
66996831
]
67006832
).toEqual({ objectId: object4.id, __typename: 'PublicClass' });
67016833
await expectAsync(object4.fetch({ useMasterKey: true })).toBeRejectedWith(
@@ -7832,6 +7964,9 @@ describe('ParseGraphQLServer', () => {
78327964
});
78337965

78347966
describe('Functions Mutations', () => {
7967+
beforeEach(async () => {
7968+
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
7969+
});
78357970
it('can be called', async () => {
78367971
try {
78377972
const clientMutationId = uuidv4();
@@ -11299,25 +11434,25 @@ describe('ParseGraphQLServer', () => {
1129911434
},
1130011435
});
1130111436
const SomeClassType = new GraphQLObjectType({
11302-
name: 'SomeClass',
11303-
fields: {
11304-
nameUpperCase: {
11305-
type: new GraphQLNonNull(GraphQLString),
11306-
resolve: p => p.name.toUpperCase(),
11307-
},
11308-
type: { type: TypeEnum },
11309-
language: {
11310-
type: new GraphQLEnumType({
11311-
name: 'LanguageEnum',
11312-
values: {
11313-
fr: { value: 'fr' },
11314-
en: { value: 'en' },
11315-
},
11316-
}),
11317-
resolve: () => 'fr',
11318-
},
11437+
name: 'SomeClass',
11438+
fields: {
11439+
nameUpperCase: {
11440+
type: new GraphQLNonNull(GraphQLString),
11441+
resolve: p => p.name.toUpperCase(),
11442+
},
11443+
type: { type: TypeEnum },
11444+
language: {
11445+
type: new GraphQLEnumType({
11446+
name: 'LanguageEnum',
11447+
values: {
11448+
fr: { value: 'fr' },
11449+
en: { value: 'en' },
11450+
},
11451+
}),
11452+
resolve: () => 'fr',
1131911453
},
11320-
}),
11454+
},
11455+
}),
1132111456
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
1132211457
graphQLPath: '/graphql',
1132311458
graphQLCustomTypeDefs: new GraphQLSchema({

src/GraphQL/ParseGraphQLServer.js

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ApolloServer } from '@apollo/server';
44
import { expressMiddleware } from '@as-integrations/express5';
55
import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';
66
import express from 'express';
7-
import { execute, subscribe, GraphQLError } from 'graphql';
7+
import { execute, subscribe, GraphQLError, parse } from 'graphql';
88
import { SubscriptionServer } from 'subscriptions-transport-ws';
99
import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
1010
import requiredParameter from '../requiredParameter';
@@ -13,6 +13,38 @@ import { ParseGraphQLSchema } from './ParseGraphQLSchema';
1313
import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController';
1414

1515

16+
const hasTypeIntrospection = (query) => {
17+
try {
18+
const ast = parse(query);
19+
// Check only root-level fields in the query
20+
// Note: selection.name.value is the actual field name, so this correctly handles
21+
// aliases like "myAlias: __type(...)" where name.value === "__type"
22+
for (const definition of ast.definitions) {
23+
if (definition.kind === 'OperationDefinition' && definition.selectionSet) {
24+
for (const selection of definition.selectionSet.selections) {
25+
if (selection.kind === 'Field' && selection.name.value === '__type') {
26+
return true;
27+
}
28+
}
29+
}
30+
}
31+
return false;
32+
} catch (e) {
33+
// If parsing fails, we assume it's not a valid query and let Apollo handle it
34+
return false;
35+
}
36+
};
37+
38+
const throwIntrospectionError = () => {
39+
throw new GraphQLError('Introspection is not allowed', {
40+
extensions: {
41+
http: {
42+
status: 403,
43+
},
44+
}
45+
});
46+
};
47+
1648
const IntrospectionControlPlugin = (publicIntrospection) => ({
1749

1850

@@ -29,21 +61,20 @@ const IntrospectionControlPlugin = (publicIntrospection) => ({
2961
return;
3062
}
3163

32-
// Now we check if the query is an introspection query
33-
// this check strategy should work in 99.99% cases
34-
// we can have an issue if a user name a field or class __schemaSomething
35-
// we want to avoid a full AST check
36-
const isIntrospectionQuery =
37-
requestContext.request.query?.includes('__schema')
38-
39-
if (isIntrospectionQuery) {
40-
throw new GraphQLError('Introspection is not allowed', {
41-
extensions: {
42-
http: {
43-
status: 403,
44-
},
45-
}
46-
});
64+
const query = requestContext.request.query;
65+
66+
67+
// Fast path: simple string check for __schema
68+
// This avoids parsing the query in most cases
69+
if (query?.includes('__schema')) {
70+
return throwIntrospectionError();
71+
}
72+
73+
// Smart check for __type: only parse if the string is present
74+
// This avoids false positives (e.g., "__type" in strings or comments)
75+
// while still being efficient for the common case
76+
if (query?.includes('__type') && hasTypeIntrospection(query)) {
77+
return throwIntrospectionError();
4778
}
4879
},
4980

0 commit comments

Comments
 (0)