mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: GraphQL __type introspection bypass via inline fragments when public introspection is disabled ([GHSA-q5q9-2rhp-33qw](https://github.com/parse-community/parse-server/security/advisories/GHSA-q5q9-2rhp-33qw)) (#10111)
This commit is contained in:
@@ -920,6 +920,52 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should block __type introspection inside inline fragment without master key', async () => {
|
||||
try {
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
query InlineFragmentBypass {
|
||||
... on Query {
|
||||
__type(name: "User") {
|
||||
name
|
||||
kind
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
fail('should have thrown an error');
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('Response not successful: Received status code 403');
|
||||
expect(e.networkError.result.errors[0].message).toEqual('Introspection is not allowed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should block __type introspection inside nested inline fragments without master key', async () => {
|
||||
try {
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
query NestedInlineFragmentBypass {
|
||||
... on Query {
|
||||
... {
|
||||
__type(name: "User") {
|
||||
name
|
||||
kind
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
fail('should have thrown an error');
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('Response not successful: Received status code 403');
|
||||
expect(e.networkError.result.errors[0].message).toEqual('Introspection is not allowed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow __type introspection with master key', async () => {
|
||||
const introspection = await apolloClient.query({
|
||||
query: gql`
|
||||
|
||||
@@ -16,25 +16,30 @@ import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/Parse
|
||||
const hasTypeIntrospection = (query) => {
|
||||
try {
|
||||
const ast = parse(query);
|
||||
// Check only root-level fields in the query
|
||||
// Note: selection.name.value is the actual field name, so this correctly handles
|
||||
// aliases like "myAlias: __type(...)" where name.value === "__type"
|
||||
for (const definition of ast.definitions) {
|
||||
if ((definition.kind === 'OperationDefinition' || definition.kind === 'FragmentDefinition') && definition.selectionSet) {
|
||||
for (const selection of definition.selectionSet.selections) {
|
||||
if (selection.kind === 'Field' && selection.name.value === '__type') {
|
||||
// GraphQL's introspection __type field requires a 'name' argument
|
||||
// This distinguishes it from potential user-defined __type fields
|
||||
if (selection.arguments && selection.arguments.length > 0) {
|
||||
return true;
|
||||
}
|
||||
const checkSelections = (selections) => {
|
||||
for (const selection of selections) {
|
||||
if (selection.kind === 'Field' && selection.name.value === '__type') {
|
||||
if (selection.arguments && selection.arguments.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (selection.selectionSet) {
|
||||
if (checkSelections(selection.selectionSet.selections)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (const definition of ast.definitions) {
|
||||
if (definition.selectionSet) {
|
||||
if (checkSelections(definition.selectionSet.selections)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
// If parsing fails, we assume it's not a valid query and let Apollo handle it
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user