diff --git a/graphql/server/src/protection/__tests__/document-gate.test.ts b/graphql/server/src/protection/__tests__/document-gate.test.ts index 40f686b7c..3d321387b 100644 --- a/graphql/server/src/protection/__tests__/document-gate.test.ts +++ b/graphql/server/src/protection/__tests__/document-gate.test.ts @@ -181,6 +181,28 @@ describe('introspection', () => { expect(() => enforce('{ __schema { queryType { name } } }')).not.toThrow(); }); + it('does not charge the standard introspection document against the depth budget', () => { + const source = ` + { __schema { types { fields { args { type { ...TypeRef } } } } } } + fragment TypeRef on __Type { + kind name + ofType { kind name ofType { kind name ofType { kind name ofType { kind name + ofType { kind name ofType { kind name ofType { kind name } } } } } } } + } + `; + expect(enforce(source, { maxQueryDepth: 3 })).toEqual({ depth: 1, cost: 0 }); + }); + + it('still measures the rest of an operation that also introspects', () => { + expect( + codeOf(() => + enforce('{ __schema { queryType { name } } user(id: "1") { manager { manager { name } } } }', { + maxQueryDepth: 3 + }) + ) + ).toBe('QUERY_TOO_DEEP'); + }); + it('never blocks __typename, which is not introspection of the schema', () => { expect(() => enforce('{ user(id: "1") { __typename name } }', { enableIntrospection: false }) diff --git a/graphql/server/src/protection/document-gate.ts b/graphql/server/src/protection/document-gate.ts index aaca15d4e..9ecbd96ac 100644 --- a/graphql/server/src/protection/document-gate.ts +++ b/graphql/server/src/protection/document-gate.ts @@ -16,6 +16,13 @@ * The walk is manual rather than `visitWithTypeInfo` because fragment spreads * have to be followed (a document can hide its depth entirely inside * fragments) and the visitor does not follow them. + * + * Schema introspection (`__schema` / `__type`) is governed only by + * `enableIntrospection`. Its selections are not walked: the introspection types + * carry no connections, so there is nothing to cost, and the standard + * introspection document nests a fixed `ofType` chain deeper than a sensible + * tenant depth budget, so charging depth would make the budget decide whether + * clients can introspect at all — a decision the dedicated switch already owns. */ import type { ConstructiveError } from '@constructive-io/errors'; @@ -119,6 +126,7 @@ function walkSelectionSet( if (!walk.protection.enableIntrospection) { reject(errors.INTROSPECTION_DISABLED()); } + continue; } const field =