From 16850e1b65abeb0e12e238de4385379d7827655c Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 11 Apr 2024 23:11:19 +0200 Subject: [PATCH 01/36] introduce rule --- src/index.ts | 1 + .../MaxIntrospectionFieldsDepthRule-test.ts | 347 ++++++++++++++++++ src/validation/index.ts | 2 + .../rules/MaxIntrospectionFieldsDepthRule.ts | 57 +++ src/validation/specifiedRules.ts | 2 + 5 files changed, 409 insertions(+) create mode 100644 src/validation/__tests__/MaxIntrospectionFieldsDepthRule-test.ts create mode 100644 src/validation/rules/MaxIntrospectionFieldsDepthRule.ts diff --git a/src/index.ts b/src/index.ts index 1a0f1b4c82..7340f67cf1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -382,6 +382,7 @@ export { ValuesOfCorrectTypeRule, VariablesAreInputTypesRule, VariablesInAllowedPositionRule, + MaxIntrospectionFieldsDepthRule, // SDL-specific validation rules LoneSchemaDefinitionRule, UniqueOperationTypesRule, diff --git a/src/validation/__tests__/MaxIntrospectionFieldsDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionFieldsDepthRule-test.ts new file mode 100644 index 0000000000..d189a7422e --- /dev/null +++ b/src/validation/__tests__/MaxIntrospectionFieldsDepthRule-test.ts @@ -0,0 +1,347 @@ +import { describe, it } from 'mocha'; + +import { getIntrospectionQuery } from '../../utilities/getIntrospectionQuery.js'; + +import { MaxIntrospectionFieldsDepthRule } from '../rules/MaxIntrospectionFieldsDepthRule.js'; + +import { expectValidationErrors } from './harness.js'; + +function expectErrors(queryStr: string) { + return expectValidationErrors(MaxIntrospectionFieldsDepthRule, queryStr); +} + +function expectValid(queryStr: string) { + expectErrors(queryStr).toDeepEqual([]); +} + +describe('Validate: Max introspection "fields" depth rule', () => { + it('default introspection query', () => { + expectValid(getIntrospectionQuery()); + }); + + it('all options introspection query', () => { + expectValid( + getIntrospectionQuery({ + descriptions: true, + specifiedByUrl: true, + directiveIsRepeatable: true, + schemaDescription: true, + inputValueDeprecation: true, + }), + ); + }); + + it('3 fields depth introspection query', () => { + expectErrors(` + { + __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth of "fields" exceeded', + }, + ]); + }); + + it('malicious introspection query', () => { + expectErrors(` + query test { + __schema { + types { + ...F1 + } + } + } + + fragment F1 on __Type { + fields { + type { + ...F2 + } + } + ofType { + ...F2 + } + } + + fragment F2 on __Type { + fields { + type { + ...F3 + } + } + ofType { + ...F3 + } + } + + fragment F3 on __Type { + fields { + type { + ...F4 + } + } + ofType { + ...F4 + } + } + + fragment F4 on __Type { + fields { + type { + ...F5 + } + } + ofType { + ...F5 + } + } + + fragment F5 on __Type { + fields { + type { + ...F6 + } + } + ofType { + ...F6 + } + } + + fragment F6 on __Type { + fields { + type { + ...F7 + } + } + ofType { + ...F7 + } + } + + fragment F7 on __Type { + fields { + type { + ...F8 + } + } + ofType { + ...F8 + } + } + + fragment F8 on __Type { + fields { + type { + ...F9 + } + } + ofType { + ...F9 + } + } + + fragment F9 on __Type { + fields { + type { + ...F10 + } + } + ofType { + ...F10 + } + } + + fragment F10 on __Type { + fields { + type { + ...F11 + } + } + ofType { + ...F11 + } + } + + fragment F11 on __Type { + fields { + type { + ...F12 + } + } + ofType { + ...F12 + } + } + + fragment F12 on __Type { + fields { + type { + ...F13 + } + } + ofType { + ...F13 + } + } + + fragment F13 on __Type { + fields { + type { + ...F14 + } + } + ofType { + ...F14 + } + } + + fragment F14 on __Type { + fields { + type { + ...F15 + } + } + ofType { + ...F15 + } + } + + fragment F15 on __Type { + fields { + type { + ...F16 + } + } + ofType { + ...F16 + } + } + + fragment F16 on __Type { + fields { + type { + ...F17 + } + } + ofType { + ...F17 + } + } + + fragment F17 on __Type { + fields { + type { + ...F18 + } + } + ofType { + ...F18 + } + } + + fragment F18 on __Type { + fields { + type { + ...F19 + } + } + ofType { + ...F19 + } + } + + fragment F19 on __Type { + fields { + type { + ...F20 + } + } + ofType { + ...F20 + } + } + + fragment F20 on __Type { + fields { + type { + ...F21 + } + } + ofType { + ...F21 + } + } + + fragment F21 on __Type { + fields { + type { + ...F22 + } + } + ofType { + ...F22 + } + } + + fragment F22 on __Type { + fields { + type { + ...F23 + } + } + ofType { + ...F23 + } + } + + fragment F23 on __Type { + fields { + type { + ...F24 + } + } + ofType { + ...F24 + } + } + + fragment F24 on __Type { + fields { + type { + ...F25 + } + } + ofType { + ...F25 + } + } + + fragment F25 on __Type { + fields { + type { + name + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth of "fields" exceeded', + }, + ]); + }); +}); diff --git a/src/validation/index.ts b/src/validation/index.ts index b0cc754490..b7f3694c18 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -96,6 +96,8 @@ export { VariablesAreInputTypesRule } from './rules/VariablesAreInputTypesRule.j // Spec Section: "All Variable Usages Are Allowed" export { VariablesInAllowedPositionRule } from './rules/VariablesInAllowedPositionRule.js'; +export { MaxIntrospectionFieldsDepthRule } from './rules/MaxIntrospectionFieldsDepthRule.js'; + // SDL-specific validation rules export { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.js'; export { UniqueOperationTypesRule } from './rules/UniqueOperationTypesRule.js'; diff --git a/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts b/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts new file mode 100644 index 0000000000..e331076353 --- /dev/null +++ b/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts @@ -0,0 +1,57 @@ +import { GraphQLError } from '../../error/GraphQLError.js'; + +import type { ASTNode } from '../../language/ast.js'; +import { Kind } from '../../language/kinds.js'; +import type { ASTVisitor } from '../../language/visitor.js'; + +import type { ASTValidationContext } from '../ValidationContext.js'; + +const MAX_INTROSPECTION_FIELDS_DEPTH = 3; + +export function MaxIntrospectionFieldsDepthRule( + context: ASTValidationContext, +): ASTVisitor { + function countDepth(node: ASTNode, count = 0) { + if (node.kind === Kind.FRAGMENT_SPREAD) { + const fragment = context.getFragment(node.name.value); + if (!fragment) { + throw new Error(`Fragment ${node.name.value} not found`); + } + count = countDepth(fragment, count); + } + + if ('selectionSet' in node && node.selectionSet) { + for (const child of node.selectionSet.selections) { + count = countDepth(child, count); + } + } + + if ('name' in node && node.name?.value === 'fields') { + count++; + } + + if (count >= MAX_INTROSPECTION_FIELDS_DEPTH) { + throw new GraphQLError( + 'Maximum introspection depth of "fields" exceeded', + ); + } + + return count; + } + + return { + Field(field) { + if (field.name.value === '__schema') { + try { + countDepth(field); + } catch (err) { + if (err instanceof GraphQLError) { + context.reportError(err); + } else { + throw err; + } + } + } + }, + }; +} diff --git a/src/validation/specifiedRules.ts b/src/validation/specifiedRules.ts index 60c967f8f0..783305f0cf 100644 --- a/src/validation/specifiedRules.ts +++ b/src/validation/specifiedRules.ts @@ -25,6 +25,7 @@ import { KnownTypeNamesRule } from './rules/KnownTypeNamesRule.js'; import { LoneAnonymousOperationRule } from './rules/LoneAnonymousOperationRule.js'; // SDL-specific validation rules import { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.js'; +import { MaxIntrospectionFieldsDepthRule } from './rules/MaxIntrospectionFieldsDepthRule.js'; // Spec Section: "Fragments must not form cycles" import { NoFragmentCyclesRule } from './rules/NoFragmentCyclesRule.js'; // Spec Section: "All Variable Used Defined" @@ -112,6 +113,7 @@ export const specifiedRules: ReadonlyArray = Object.freeze([ VariablesInAllowedPositionRule, OverlappingFieldsCanBeMergedRule, UniqueInputFieldNamesRule, + MaxIntrospectionFieldsDepthRule, ]); /** From 5fe9959ef817db73be65b619cc71bbe6e80889fe Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 15:10:58 +0200 Subject: [PATCH 02/36] also __type --- src/validation/rules/MaxIntrospectionFieldsDepthRule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts b/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts index e331076353..8c112ce14e 100644 --- a/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts @@ -41,7 +41,7 @@ export function MaxIntrospectionFieldsDepthRule( return { Field(field) { - if (field.name.value === '__schema') { + if (field.name.value === '__schema' || field.name.value === '__type ') { try { countDepth(field); } catch (err) { From f2a56e72c7169b038a92ad2410afe4003f8cb15f Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 15:11:23 +0200 Subject: [PATCH 03/36] shorter error --- .../__tests__/MaxIntrospectionFieldsDepthRule-test.ts | 4 ++-- src/validation/rules/MaxIntrospectionFieldsDepthRule.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/validation/__tests__/MaxIntrospectionFieldsDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionFieldsDepthRule-test.ts index d189a7422e..8bc4e4a934 100644 --- a/src/validation/__tests__/MaxIntrospectionFieldsDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionFieldsDepthRule-test.ts @@ -52,7 +52,7 @@ describe('Validate: Max introspection "fields" depth rule', () => { } `).toDeepEqual([ { - message: 'Maximum introspection depth of "fields" exceeded', + message: 'Maximum introspection depth exceeded', }, ]); }); @@ -340,7 +340,7 @@ describe('Validate: Max introspection "fields" depth rule', () => { } `).toDeepEqual([ { - message: 'Maximum introspection depth of "fields" exceeded', + message: 'Maximum introspection depth exceeded', }, ]); }); diff --git a/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts b/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts index 8c112ce14e..2921a9a8a1 100644 --- a/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts @@ -31,9 +31,7 @@ export function MaxIntrospectionFieldsDepthRule( } if (count >= MAX_INTROSPECTION_FIELDS_DEPTH) { - throw new GraphQLError( - 'Maximum introspection depth of "fields" exceeded', - ); + throw new GraphQLError('Maximum introspection depth exceeded'); } return count; From 3b90c852068a96d27965a1f65f426be45191bb7d Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 15:13:15 +0200 Subject: [PATCH 04/36] fragment spread wont have more selections --- src/validation/rules/MaxIntrospectionFieldsDepthRule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts b/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts index 2921a9a8a1..7d6fb8fc76 100644 --- a/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts @@ -17,7 +17,7 @@ export function MaxIntrospectionFieldsDepthRule( if (!fragment) { throw new Error(`Fragment ${node.name.value} not found`); } - count = countDepth(fragment, count); + return countDepth(fragment, count); } if ('selectionSet' in node && node.selectionSet) { From d3889924a7ba70e68a712ccbb8c68446481c46cc Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 15:14:25 +0200 Subject: [PATCH 05/36] MaxIntrospectionDepthRule --- src/index.ts | 2 +- ...sDepthRule-test.ts => MaxIntrospectionDepthRule-test.ts} | 6 +++--- src/validation/index.ts | 2 +- ...ctionFieldsDepthRule.ts => MaxIntrospectionDepthRule.ts} | 2 +- src/validation/specifiedRules.ts | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename src/validation/__tests__/{MaxIntrospectionFieldsDepthRule-test.ts => MaxIntrospectionDepthRule-test.ts} (95%) rename src/validation/rules/{MaxIntrospectionFieldsDepthRule.ts => MaxIntrospectionDepthRule.ts} (96%) diff --git a/src/index.ts b/src/index.ts index 7340f67cf1..c90a1a5fc9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -382,7 +382,7 @@ export { ValuesOfCorrectTypeRule, VariablesAreInputTypesRule, VariablesInAllowedPositionRule, - MaxIntrospectionFieldsDepthRule, + MaxIntrospectionDepthRule, // SDL-specific validation rules LoneSchemaDefinitionRule, UniqueOperationTypesRule, diff --git a/src/validation/__tests__/MaxIntrospectionFieldsDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts similarity index 95% rename from src/validation/__tests__/MaxIntrospectionFieldsDepthRule-test.ts rename to src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index 8bc4e4a934..6fd15f4a9c 100644 --- a/src/validation/__tests__/MaxIntrospectionFieldsDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -2,19 +2,19 @@ import { describe, it } from 'mocha'; import { getIntrospectionQuery } from '../../utilities/getIntrospectionQuery.js'; -import { MaxIntrospectionFieldsDepthRule } from '../rules/MaxIntrospectionFieldsDepthRule.js'; +import { MaxIntrospectionDepthRule } from '../rules/MaxIntrospectionDepthRule.js'; import { expectValidationErrors } from './harness.js'; function expectErrors(queryStr: string) { - return expectValidationErrors(MaxIntrospectionFieldsDepthRule, queryStr); + return expectValidationErrors(MaxIntrospectionDepthRule, queryStr); } function expectValid(queryStr: string) { expectErrors(queryStr).toDeepEqual([]); } -describe('Validate: Max introspection "fields" depth rule', () => { +describe('Validate: Max introspection depth rule', () => { it('default introspection query', () => { expectValid(getIntrospectionQuery()); }); diff --git a/src/validation/index.ts b/src/validation/index.ts index b7f3694c18..fae63df69b 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -96,7 +96,7 @@ export { VariablesAreInputTypesRule } from './rules/VariablesAreInputTypesRule.j // Spec Section: "All Variable Usages Are Allowed" export { VariablesInAllowedPositionRule } from './rules/VariablesInAllowedPositionRule.js'; -export { MaxIntrospectionFieldsDepthRule } from './rules/MaxIntrospectionFieldsDepthRule.js'; +export { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; // SDL-specific validation rules export { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.js'; diff --git a/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts similarity index 96% rename from src/validation/rules/MaxIntrospectionFieldsDepthRule.ts rename to src/validation/rules/MaxIntrospectionDepthRule.ts index 7d6fb8fc76..9d2178c9c4 100644 --- a/src/validation/rules/MaxIntrospectionFieldsDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -8,7 +8,7 @@ import type { ASTValidationContext } from '../ValidationContext.js'; const MAX_INTROSPECTION_FIELDS_DEPTH = 3; -export function MaxIntrospectionFieldsDepthRule( +export function MaxIntrospectionDepthRule( context: ASTValidationContext, ): ASTVisitor { function countDepth(node: ASTNode, count = 0) { diff --git a/src/validation/specifiedRules.ts b/src/validation/specifiedRules.ts index 783305f0cf..e88a39deef 100644 --- a/src/validation/specifiedRules.ts +++ b/src/validation/specifiedRules.ts @@ -25,7 +25,7 @@ import { KnownTypeNamesRule } from './rules/KnownTypeNamesRule.js'; import { LoneAnonymousOperationRule } from './rules/LoneAnonymousOperationRule.js'; // SDL-specific validation rules import { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.js'; -import { MaxIntrospectionFieldsDepthRule } from './rules/MaxIntrospectionFieldsDepthRule.js'; +import { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; // Spec Section: "Fragments must not form cycles" import { NoFragmentCyclesRule } from './rules/NoFragmentCyclesRule.js'; // Spec Section: "All Variable Used Defined" @@ -113,7 +113,7 @@ export const specifiedRules: ReadonlyArray = Object.freeze([ VariablesInAllowedPositionRule, OverlappingFieldsCanBeMergedRule, UniqueInputFieldNamesRule, - MaxIntrospectionFieldsDepthRule, + MaxIntrospectionDepthRule, ]); /** From 8f45b7580dbffcc93e2bfd6887ba64888c09f198 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 17:18:57 +0200 Subject: [PATCH 06/36] count or reach --- .../rules/MaxIntrospectionDepthRule.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 9d2178c9c4..28488a4ee0 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -11,18 +11,28 @@ const MAX_INTROSPECTION_FIELDS_DEPTH = 3; export function MaxIntrospectionDepthRule( context: ASTValidationContext, ): ASTVisitor { - function countDepth(node: ASTNode, count = 0) { + /** + * Counts the "fields" recursively and returns `true` if the + * limit has been reached; otherwise, returns the count. + */ + function countDepth(node: ASTNode): number | true { + let count = 0; + if (node.kind === Kind.FRAGMENT_SPREAD) { const fragment = context.getFragment(node.name.value); if (!fragment) { throw new Error(`Fragment ${node.name.value} not found`); } - return countDepth(fragment, count); + return countDepth(fragment); } if ('selectionSet' in node && node.selectionSet) { for (const child of node.selectionSet.selections) { - count = countDepth(child, count); + const countOrReached = countDepth(child); + if (countOrReached === true) { + return true; + } + count += countOrReached; } } @@ -31,7 +41,7 @@ export function MaxIntrospectionDepthRule( } if (count >= MAX_INTROSPECTION_FIELDS_DEPTH) { - throw new GraphQLError('Maximum introspection depth exceeded'); + return true; } return count; @@ -40,14 +50,11 @@ export function MaxIntrospectionDepthRule( return { Field(field) { if (field.name.value === '__schema' || field.name.value === '__type ') { - try { - countDepth(field); - } catch (err) { - if (err instanceof GraphQLError) { - context.reportError(err); - } else { - throw err; - } + const reached = countDepth(field); + if (reached === true) { + context.reportError( + new GraphQLError('Maximum introspection depth exceeded'), + ); } } }, From 48cb905775d0dcc8abc9b84a8e4ea0ca2e9586d6 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 17:45:07 +0200 Subject: [PATCH 07/36] ultra shorter --- .../rules/MaxIntrospectionDepthRule.ts | 76 +++++++------------ 1 file changed, 26 insertions(+), 50 deletions(-) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 28488a4ee0..1e68013feb 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -1,62 +1,38 @@ import { GraphQLError } from '../../error/GraphQLError.js'; -import type { ASTNode } from '../../language/ast.js'; -import { Kind } from '../../language/kinds.js'; import type { ASTVisitor } from '../../language/visitor.js'; +import { BREAK } from '../../language/visitor.js'; -import type { ASTValidationContext } from '../ValidationContext.js'; +import { __Type } from '../../type/introspection.js'; -const MAX_INTROSPECTION_FIELDS_DEPTH = 3; +import { TypeInfo, visitWithTypeInfo } from '../../utilities/TypeInfo.js'; -export function MaxIntrospectionDepthRule( - context: ASTValidationContext, -): ASTVisitor { - /** - * Counts the "fields" recursively and returns `true` if the - * limit has been reached; otherwise, returns the count. - */ - function countDepth(node: ASTNode): number | true { - let count = 0; - - if (node.kind === Kind.FRAGMENT_SPREAD) { - const fragment = context.getFragment(node.name.value); - if (!fragment) { - throw new Error(`Fragment ${node.name.value} not found`); - } - return countDepth(fragment); - } - - if ('selectionSet' in node && node.selectionSet) { - for (const child of node.selectionSet.selections) { - const countOrReached = countDepth(child); - if (countOrReached === true) { - return true; - } - count += countOrReached; - } - } - - if ('name' in node && node.name?.value === 'fields') { - count++; - } +import type { SDLValidationContext } from '../ValidationContext.js'; - if (count >= MAX_INTROSPECTION_FIELDS_DEPTH) { - return true; - } +/** Maximum number of "__Type.fields" appearances during introspection. */ +const MAX_TYPE_FIELDS_COUNT = 3; - return count; +export function MaxIntrospectionDepthRule( + context: SDLValidationContext, +): ASTVisitor { + const schema = context.getSchema(); + if (!schema) { + throw new Error('Max introspection depth rule must have a schema'); } - - return { - Field(field) { - if (field.name.value === '__schema' || field.name.value === '__type ') { - const reached = countDepth(field); - if (reached === true) { - context.reportError( - new GraphQLError('Maximum introspection depth exceeded'), - ); - } + const typeInfo = new TypeInfo(schema); + let count = 0; + return visitWithTypeInfo(typeInfo, { + Field(field, _key) { + if ( + field.name.value === 'fields' && + typeInfo.getParentType() === __Type && + ++count >= MAX_TYPE_FIELDS_COUNT + ) { + context.reportError( + new GraphQLError('Maximum introspection depth exceeded'), + ); + return BREAK; } }, - }; + }); } From 290091afdfd5c3072811b6f165955fb539375123 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 17:53:39 +0200 Subject: [PATCH 08/36] unused arg --- src/validation/rules/MaxIntrospectionDepthRule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 1e68013feb..1cec687c12 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -22,7 +22,7 @@ export function MaxIntrospectionDepthRule( const typeInfo = new TypeInfo(schema); let count = 0; return visitWithTypeInfo(typeInfo, { - Field(field, _key) { + Field(field) { if ( field.name.value === 'fields' && typeInfo.getParentType() === __Type && From 860e1f607742c8540ca57a68639324f7441e4e15 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 17:55:52 +0200 Subject: [PATCH 09/36] validationcontext --- src/validation/rules/MaxIntrospectionDepthRule.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 1cec687c12..428f0cb794 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -7,19 +7,15 @@ import { __Type } from '../../type/introspection.js'; import { TypeInfo, visitWithTypeInfo } from '../../utilities/TypeInfo.js'; -import type { SDLValidationContext } from '../ValidationContext.js'; +import type { ValidationContext } from '../ValidationContext.js'; /** Maximum number of "__Type.fields" appearances during introspection. */ const MAX_TYPE_FIELDS_COUNT = 3; export function MaxIntrospectionDepthRule( - context: SDLValidationContext, + context: ValidationContext, ): ASTVisitor { - const schema = context.getSchema(); - if (!schema) { - throw new Error('Max introspection depth rule must have a schema'); - } - const typeInfo = new TypeInfo(schema); + const typeInfo = new TypeInfo(context.getSchema()); let count = 0; return visitWithTypeInfo(typeInfo, { Field(field) { From 1d5f90a92ba5cc61b8c74f58b1b6fee3368ae929 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 17:59:53 +0200 Subject: [PATCH 10/36] max fields 4 appearances --- src/validation/__tests__/MaxIntrospectionDepthRule-test.ts | 6 +++++- src/validation/rules/MaxIntrospectionDepthRule.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index 6fd15f4a9c..921a002f37 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -41,7 +41,11 @@ describe('Validate: Max introspection depth rule', () => { fields { type { fields { - name + type { + fields { + name + } + } } } } diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 428f0cb794..08519db48a 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -10,7 +10,7 @@ import { TypeInfo, visitWithTypeInfo } from '../../utilities/TypeInfo.js'; import type { ValidationContext } from '../ValidationContext.js'; /** Maximum number of "__Type.fields" appearances during introspection. */ -const MAX_TYPE_FIELDS_COUNT = 3; +const MAX_TYPE_FIELDS_COUNT = 4; export function MaxIntrospectionDepthRule( context: ValidationContext, From c7b84f1f7cacac46f9caf6bc99e44ce577330cd1 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 18:07:27 +0200 Subject: [PATCH 11/36] failing test --- .../MaxIntrospectionDepthRule-test.ts | 24 +++++++++++++++---- .../rules/MaxIntrospectionDepthRule.ts | 2 +- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index 921a002f37..320e3359ca 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -31,6 +31,24 @@ describe('Validate: Max introspection depth rule', () => { ); }); + it('3 flat fields introspection query', () => { + expectValid(` + { + __type(name: "Query") { + trueFields: fields(includeDeprecated: true) { + name + } + falseFields: fields(includeDeprecated: false) { + name + } + omittedFields: fields { + name + } + } + } + `); + }); + it('3 fields depth introspection query', () => { expectErrors(` { @@ -41,11 +59,7 @@ describe('Validate: Max introspection depth rule', () => { fields { type { fields { - type { - fields { - name - } - } + name } } } diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 08519db48a..428f0cb794 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -10,7 +10,7 @@ import { TypeInfo, visitWithTypeInfo } from '../../utilities/TypeInfo.js'; import type { ValidationContext } from '../ValidationContext.js'; /** Maximum number of "__Type.fields" appearances during introspection. */ -const MAX_TYPE_FIELDS_COUNT = 4; +const MAX_TYPE_FIELDS_COUNT = 3; export function MaxIntrospectionDepthRule( context: ValidationContext, From b09145ba0743abb1bcfc39a63b54abe753020a72 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 18:39:49 +0200 Subject: [PATCH 12/36] back to counter --- .../MaxIntrospectionDepthRule-test.ts | 36 +++++++++++++++++-- .../rules/MaxIntrospectionDepthRule.ts | 2 +- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index 320e3359ca..e38626828b 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -49,7 +49,7 @@ describe('Validate: Max introspection depth rule', () => { `); }); - it('3 fields depth introspection query', () => { + it('4 fields depth introspection query', () => { expectErrors(` { __schema { @@ -59,7 +59,11 @@ describe('Validate: Max introspection depth rule', () => { fields { type { fields { - name + type { + fields { + name + } + } } } } @@ -75,6 +79,34 @@ describe('Validate: Max introspection depth rule', () => { ]); }); + it('1 fields deep with 3 fields introspection query', () => { + expectErrors(` + { + __schema { + types { + fields { + type { + oneFields: fields { + name + } + twoFields: fields { + name + } + threeFields: fields { + name + } + } + } + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + }, + ]); + }); + it('malicious introspection query', () => { expectErrors(` query test { diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 428f0cb794..36d77c3ac9 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -22,7 +22,7 @@ export function MaxIntrospectionDepthRule( if ( field.name.value === 'fields' && typeInfo.getParentType() === __Type && - ++count >= MAX_TYPE_FIELDS_COUNT + ++count > MAX_TYPE_FIELDS_COUNT ) { context.reportError( new GraphQLError('Maximum introspection depth exceeded'), From b63e328d53c1b23e22016b5695cff4e183ec98de Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 18:43:09 +0200 Subject: [PATCH 13/36] max introspection nodes --- src/index.ts | 2 +- ...e-test.ts => MaxIntrospectionNodesRule-test.ts} | 14 +++++++------- src/validation/index.ts | 2 +- ...onDepthRule.ts => MaxIntrospectionNodesRule.ts} | 4 ++-- src/validation/specifiedRules.ts | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) rename src/validation/__tests__/{MaxIntrospectionDepthRule-test.ts => MaxIntrospectionNodesRule-test.ts} (92%) rename src/validation/rules/{MaxIntrospectionDepthRule.ts => MaxIntrospectionNodesRule.ts} (89%) diff --git a/src/index.ts b/src/index.ts index c90a1a5fc9..d622eead3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -382,7 +382,7 @@ export { ValuesOfCorrectTypeRule, VariablesAreInputTypesRule, VariablesInAllowedPositionRule, - MaxIntrospectionDepthRule, + MaxIntrospectionNodesRule, // SDL-specific validation rules LoneSchemaDefinitionRule, UniqueOperationTypesRule, diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts similarity index 92% rename from src/validation/__tests__/MaxIntrospectionDepthRule-test.ts rename to src/validation/__tests__/MaxIntrospectionNodesRule-test.ts index e38626828b..03bd5bfaa1 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts @@ -2,19 +2,19 @@ import { describe, it } from 'mocha'; import { getIntrospectionQuery } from '../../utilities/getIntrospectionQuery.js'; -import { MaxIntrospectionDepthRule } from '../rules/MaxIntrospectionDepthRule.js'; +import { MaxIntrospectionNodesRule } from '../rules/MaxIntrospectionNodesRule.js'; import { expectValidationErrors } from './harness.js'; function expectErrors(queryStr: string) { - return expectValidationErrors(MaxIntrospectionDepthRule, queryStr); + return expectValidationErrors(MaxIntrospectionNodesRule, queryStr); } function expectValid(queryStr: string) { expectErrors(queryStr).toDeepEqual([]); } -describe('Validate: Max introspection depth rule', () => { +describe('Validate: Max introspection nodes rule', () => { it('default introspection query', () => { expectValid(getIntrospectionQuery()); }); @@ -49,7 +49,7 @@ describe('Validate: Max introspection depth rule', () => { `); }); - it('4 fields depth introspection query', () => { + it('4 fields nodes introspection query', () => { expectErrors(` { __schema { @@ -74,7 +74,7 @@ describe('Validate: Max introspection depth rule', () => { } `).toDeepEqual([ { - message: 'Maximum introspection depth exceeded', + message: 'Maximum introspection nodes exceeded', }, ]); }); @@ -102,7 +102,7 @@ describe('Validate: Max introspection depth rule', () => { } `).toDeepEqual([ { - message: 'Maximum introspection depth exceeded', + message: 'Maximum introspection nodes exceeded', }, ]); }); @@ -390,7 +390,7 @@ describe('Validate: Max introspection depth rule', () => { } `).toDeepEqual([ { - message: 'Maximum introspection depth exceeded', + message: 'Maximum introspection nodes exceeded', }, ]); }); diff --git a/src/validation/index.ts b/src/validation/index.ts index fae63df69b..5e30e0c9d2 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -96,7 +96,7 @@ export { VariablesAreInputTypesRule } from './rules/VariablesAreInputTypesRule.j // Spec Section: "All Variable Usages Are Allowed" export { VariablesInAllowedPositionRule } from './rules/VariablesInAllowedPositionRule.js'; -export { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; +export { MaxIntrospectionNodesRule } from './rules/MaxIntrospectionNodesRule.js'; // SDL-specific validation rules export { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.js'; diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionNodesRule.ts similarity index 89% rename from src/validation/rules/MaxIntrospectionDepthRule.ts rename to src/validation/rules/MaxIntrospectionNodesRule.ts index 36d77c3ac9..3c75734deb 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionNodesRule.ts @@ -12,7 +12,7 @@ import type { ValidationContext } from '../ValidationContext.js'; /** Maximum number of "__Type.fields" appearances during introspection. */ const MAX_TYPE_FIELDS_COUNT = 3; -export function MaxIntrospectionDepthRule( +export function MaxIntrospectionNodesRule( context: ValidationContext, ): ASTVisitor { const typeInfo = new TypeInfo(context.getSchema()); @@ -25,7 +25,7 @@ export function MaxIntrospectionDepthRule( ++count > MAX_TYPE_FIELDS_COUNT ) { context.reportError( - new GraphQLError('Maximum introspection depth exceeded'), + new GraphQLError('Maximum introspection nodes exceeded'), ); return BREAK; } diff --git a/src/validation/specifiedRules.ts b/src/validation/specifiedRules.ts index e88a39deef..b164cf46da 100644 --- a/src/validation/specifiedRules.ts +++ b/src/validation/specifiedRules.ts @@ -25,7 +25,7 @@ import { KnownTypeNamesRule } from './rules/KnownTypeNamesRule.js'; import { LoneAnonymousOperationRule } from './rules/LoneAnonymousOperationRule.js'; // SDL-specific validation rules import { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.js'; -import { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; +import { MaxIntrospectionNodesRule } from './rules/MaxIntrospectionNodesRule.js'; // Spec Section: "Fragments must not form cycles" import { NoFragmentCyclesRule } from './rules/NoFragmentCyclesRule.js'; // Spec Section: "All Variable Used Defined" @@ -113,7 +113,7 @@ export const specifiedRules: ReadonlyArray = Object.freeze([ VariablesInAllowedPositionRule, OverlappingFieldsCanBeMergedRule, UniqueInputFieldNamesRule, - MaxIntrospectionDepthRule, + MaxIntrospectionNodesRule, ]); /** From 363257d2bc609530b9a5e24476da9cc6f6d74ca8 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 18:44:57 +0200 Subject: [PATCH 14/36] typo --- src/validation/__tests__/MaxIntrospectionNodesRule-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts b/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts index 03bd5bfaa1..1b03c05fdd 100644 --- a/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts @@ -49,7 +49,7 @@ describe('Validate: Max introspection nodes rule', () => { `); }); - it('4 fields nodes introspection query', () => { + it('4 fields deep introspection query', () => { expectErrors(` { __schema { From 38f763872a400e62d4df1bb78b34372f73e96ad9 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 15 Apr 2024 18:45:42 +0200 Subject: [PATCH 15/36] simpler --- .../MaxIntrospectionNodesRule-test.ts | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts b/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts index 1b03c05fdd..7402fcdebd 100644 --- a/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts @@ -6,12 +6,19 @@ import { MaxIntrospectionNodesRule } from '../rules/MaxIntrospectionNodesRule.js import { expectValidationErrors } from './harness.js'; -function expectErrors(queryStr: string) { - return expectValidationErrors(MaxIntrospectionNodesRule, queryStr); +function expectInvalid(queryStr: string) { + return expectValidationErrors( + MaxIntrospectionNodesRule, + queryStr, + ).toDeepEqual([ + { + message: 'Maximum introspection nodes exceeded', + }, + ]); } function expectValid(queryStr: string) { - expectErrors(queryStr).toDeepEqual([]); + expectValidationErrors(MaxIntrospectionNodesRule, queryStr).toDeepEqual([]); } describe('Validate: Max introspection nodes rule', () => { @@ -50,7 +57,7 @@ describe('Validate: Max introspection nodes rule', () => { }); it('4 fields deep introspection query', () => { - expectErrors(` + expectInvalid(` { __schema { types { @@ -72,15 +79,11 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `).toDeepEqual([ - { - message: 'Maximum introspection nodes exceeded', - }, - ]); + `); }); it('1 fields deep with 3 fields introspection query', () => { - expectErrors(` + expectInvalid(` { __schema { types { @@ -100,15 +103,11 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `).toDeepEqual([ - { - message: 'Maximum introspection nodes exceeded', - }, - ]); + `); }); it('malicious introspection query', () => { - expectErrors(` + expectInvalid(` query test { __schema { types { @@ -388,10 +387,6 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `).toDeepEqual([ - { - message: 'Maximum introspection nodes exceeded', - }, - ]); + `); }); }); From 8ede99ff2630a2a7e7ffd9ecacf564a249432f38 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 22 Apr 2024 16:25:35 +0200 Subject: [PATCH 16/36] introspection depth --- src/index.ts | 2 +- ...t.ts => MaxIntrospectionDepthRule-test.ts} | 144 ++++++++++++++++-- src/validation/index.ts | 2 +- .../rules/MaxIntrospectionDepthRule.ts | 69 +++++++++ .../rules/MaxIntrospectionNodesRule.ts | 34 ----- src/validation/specifiedRules.ts | 4 +- 6 files changed, 206 insertions(+), 49 deletions(-) rename src/validation/__tests__/{MaxIntrospectionNodesRule-test.ts => MaxIntrospectionDepthRule-test.ts} (67%) create mode 100644 src/validation/rules/MaxIntrospectionDepthRule.ts delete mode 100644 src/validation/rules/MaxIntrospectionNodesRule.ts diff --git a/src/index.ts b/src/index.ts index d622eead3f..c90a1a5fc9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -382,7 +382,7 @@ export { ValuesOfCorrectTypeRule, VariablesAreInputTypesRule, VariablesInAllowedPositionRule, - MaxIntrospectionNodesRule, + MaxIntrospectionDepthRule, // SDL-specific validation rules LoneSchemaDefinitionRule, UniqueOperationTypesRule, diff --git a/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts similarity index 67% rename from src/validation/__tests__/MaxIntrospectionNodesRule-test.ts rename to src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index 7402fcdebd..0fed191cf3 100644 --- a/src/validation/__tests__/MaxIntrospectionNodesRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -2,23 +2,23 @@ import { describe, it } from 'mocha'; import { getIntrospectionQuery } from '../../utilities/getIntrospectionQuery.js'; -import { MaxIntrospectionNodesRule } from '../rules/MaxIntrospectionNodesRule.js'; +import { MaxIntrospectionDepthRule } from '../rules/MaxIntrospectionDepthRule.js'; import { expectValidationErrors } from './harness.js'; function expectInvalid(queryStr: string) { return expectValidationErrors( - MaxIntrospectionNodesRule, + MaxIntrospectionDepthRule, queryStr, ).toDeepEqual([ { - message: 'Maximum introspection nodes exceeded', + message: 'Maximum introspection depth exceeded', }, ]); } function expectValid(queryStr: string) { - expectValidationErrors(MaxIntrospectionNodesRule, queryStr).toDeepEqual([]); + expectValidationErrors(MaxIntrospectionDepthRule, queryStr).toDeepEqual([]); } describe('Validate: Max introspection nodes rule', () => { @@ -56,7 +56,7 @@ describe('Validate: Max introspection nodes rule', () => { `); }); - it('4 fields deep introspection query', () => { + it('3 fields deep introspection query from __schema', () => { expectInvalid(` { __schema { @@ -66,11 +66,7 @@ describe('Validate: Max introspection nodes rule', () => { fields { type { fields { - type { - fields { - name - } - } + name } } } @@ -82,8 +78,134 @@ describe('Validate: Max introspection nodes rule', () => { `); }); - it('1 fields deep with 3 fields introspection query', () => { + it('3 fields deep introspection query from multiple __schema', () => { + expectInvalid(` + { + one: __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + two: __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + three: __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } + `); + }); + + it('3 fields deep introspection query from __type', () => { + expectInvalid(` + { + __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } + `); + }); + + it('3 fields deep introspection query from multiple __type', () => { expectInvalid(` + { + one: __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + two: __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + three: __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } + `); + }); + + it('1 fields deep with 3 fields introspection query', () => { + expectValid(` { __schema { types { diff --git a/src/validation/index.ts b/src/validation/index.ts index 5e30e0c9d2..fae63df69b 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -96,7 +96,7 @@ export { VariablesAreInputTypesRule } from './rules/VariablesAreInputTypesRule.j // Spec Section: "All Variable Usages Are Allowed" export { VariablesInAllowedPositionRule } from './rules/VariablesInAllowedPositionRule.js'; -export { MaxIntrospectionNodesRule } from './rules/MaxIntrospectionNodesRule.js'; +export { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; // SDL-specific validation rules export { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.js'; diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts new file mode 100644 index 0000000000..113c493203 --- /dev/null +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -0,0 +1,69 @@ +import { GraphQLError } from '../../error/GraphQLError.js'; + +import type { ASTNode } from '../../language/ast.js'; +import { Kind } from '../../language/kinds.js'; +import type { ASTVisitor } from '../../language/visitor.js'; +import { BREAK } from '../../language/visitor.js'; + +import { + SchemaMetaFieldDef, + TypeMetaFieldDef, +} from '../../type/introspection.js'; + +import type { ValidationContext } from '../ValidationContext.js'; + +const MAX_FIELDS_DEPTH = 3; + +export function MaxIntrospectionDepthRule( + context: ValidationContext, +): ASTVisitor { + /** + * Counts the depth of "__Type.fields" recursively and + * returns `true` if the limit has been reached. + */ + function checkFieldsDepth(node: ASTNode, depth: number = 0): boolean { + if (node.kind === Kind.FRAGMENT_SPREAD) { + const fragment = context.getFragment(node.name.value); + if (!fragment) { + throw new Error(`Fragment ${node.name.value} not found`); + } + return checkFieldsDepth(fragment, depth); + } + + if ( + 'name' in node && + node.name?.value === 'fields' && + // eslint-disable-next-line no-param-reassign + ++depth >= MAX_FIELDS_DEPTH + ) { + return true; + } + + if ('selectionSet' in node && node.selectionSet) { + for (const child of node.selectionSet.selections) { + if (checkFieldsDepth(child, depth)) { + return true; + } + } + } + + return false; + } + + return { + Field(field) { + if ( + [SchemaMetaFieldDef.name, TypeMetaFieldDef.name].includes( + field.name.value, + ) + ) { + if (checkFieldsDepth(field)) { + context.reportError( + new GraphQLError('Maximum introspection depth exceeded'), + ); + return BREAK; + } + } + }, + }; +} diff --git a/src/validation/rules/MaxIntrospectionNodesRule.ts b/src/validation/rules/MaxIntrospectionNodesRule.ts deleted file mode 100644 index 3c75734deb..0000000000 --- a/src/validation/rules/MaxIntrospectionNodesRule.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { GraphQLError } from '../../error/GraphQLError.js'; - -import type { ASTVisitor } from '../../language/visitor.js'; -import { BREAK } from '../../language/visitor.js'; - -import { __Type } from '../../type/introspection.js'; - -import { TypeInfo, visitWithTypeInfo } from '../../utilities/TypeInfo.js'; - -import type { ValidationContext } from '../ValidationContext.js'; - -/** Maximum number of "__Type.fields" appearances during introspection. */ -const MAX_TYPE_FIELDS_COUNT = 3; - -export function MaxIntrospectionNodesRule( - context: ValidationContext, -): ASTVisitor { - const typeInfo = new TypeInfo(context.getSchema()); - let count = 0; - return visitWithTypeInfo(typeInfo, { - Field(field) { - if ( - field.name.value === 'fields' && - typeInfo.getParentType() === __Type && - ++count > MAX_TYPE_FIELDS_COUNT - ) { - context.reportError( - new GraphQLError('Maximum introspection nodes exceeded'), - ); - return BREAK; - } - }, - }); -} diff --git a/src/validation/specifiedRules.ts b/src/validation/specifiedRules.ts index b164cf46da..e88a39deef 100644 --- a/src/validation/specifiedRules.ts +++ b/src/validation/specifiedRules.ts @@ -25,7 +25,7 @@ import { KnownTypeNamesRule } from './rules/KnownTypeNamesRule.js'; import { LoneAnonymousOperationRule } from './rules/LoneAnonymousOperationRule.js'; // SDL-specific validation rules import { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.js'; -import { MaxIntrospectionNodesRule } from './rules/MaxIntrospectionNodesRule.js'; +import { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; // Spec Section: "Fragments must not form cycles" import { NoFragmentCyclesRule } from './rules/NoFragmentCyclesRule.js'; // Spec Section: "All Variable Used Defined" @@ -113,7 +113,7 @@ export const specifiedRules: ReadonlyArray = Object.freeze([ VariablesInAllowedPositionRule, OverlappingFieldsCanBeMergedRule, UniqueInputFieldNamesRule, - MaxIntrospectionNodesRule, + MaxIntrospectionDepthRule, ]); /** From d9e48f6cafe8d6ed19958b535ab953f48988f836 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 22 Apr 2024 16:41:47 +0200 Subject: [PATCH 17/36] varying parents and no copy-paste query --- .../MaxIntrospectionDepthRule-test.ts | 284 ++---------------- 1 file changed, 29 insertions(+), 255 deletions(-) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index 0fed191cf3..47154b20ce 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -228,285 +228,59 @@ describe('Validate: Max introspection nodes rule', () => { `); }); - it('malicious introspection query', () => { + it('3 fields deep from varying parents introspection query', () => { expectInvalid(` - query test { + { __schema { types { - ...F1 - } - } - } - - fragment F1 on __Type { - fields { - type { - ...F2 - } - } - ofType { - ...F2 - } - } - - fragment F2 on __Type { - fields { - type { - ...F3 - } - } - ofType { - ...F3 - } - } - - fragment F3 on __Type { - fields { - type { - ...F4 - } - } - ofType { - ...F4 - } - } - - fragment F4 on __Type { - fields { - type { - ...F5 - } - } - ofType { - ...F5 - } - } - - fragment F5 on __Type { - fields { - type { - ...F6 - } - } - ofType { - ...F6 - } - } - - fragment F6 on __Type { - fields { - type { - ...F7 - } - } - ofType { - ...F7 - } - } - - fragment F7 on __Type { - fields { - type { - ...F8 - } - } - ofType { - ...F8 - } - } - - fragment F8 on __Type { - fields { - type { - ...F9 - } - } - ofType { - ...F9 - } - } - - fragment F9 on __Type { - fields { - type { - ...F10 - } - } - ofType { - ...F10 - } - } - - fragment F10 on __Type { - fields { - type { - ...F11 - } - } - ofType { - ...F11 - } - } - - fragment F11 on __Type { - fields { - type { - ...F12 - } - } - ofType { - ...F12 - } - } - - fragment F12 on __Type { - fields { - type { - ...F13 - } - } - ofType { - ...F13 - } - } - - fragment F13 on __Type { - fields { - type { - ...F14 - } - } - ofType { - ...F14 - } - } - - fragment F14 on __Type { - fields { - type { - ...F15 - } - } - ofType { - ...F15 - } - } - - fragment F15 on __Type { - fields { - type { - ...F16 - } - } - ofType { - ...F16 - } - } - - fragment F16 on __Type { - fields { - type { - ...F17 - } - } - ofType { - ...F17 - } - } - - fragment F17 on __Type { - fields { - type { - ...F18 - } - } - ofType { - ...F18 - } - } - - fragment F18 on __Type { - fields { - type { - ...F19 - } - } - ofType { - ...F19 - } - } - - fragment F19 on __Type { - fields { - type { - ...F20 - } - } - ofType { - ...F20 - } - } - - fragment F20 on __Type { - fields { - type { - ...F21 - } - } - ofType { - ...F21 - } - } - - fragment F21 on __Type { - fields { - type { - ...F22 + fields { + type { + fields { + type { + ofType { + fields { + name + } + } + } + } + } + } } } - ofType { - ...F22 - } } + `); + }); - fragment F22 on __Type { - fields { - type { - ...F23 + it('3 fields deep introspection query with fragments', () => { + expectInvalid(` + query test { + __schema { + types { + ...One } } - ofType { - ...F23 - } } - fragment F23 on __Type { + fragment One on __Type { fields { type { - ...F24 + ...Two } } - ofType { - ...F24 - } } - fragment F24 on __Type { + fragment Two on __Type { fields { type { - ...F25 + ...Three } } - ofType { - ...F25 - } } - fragment F25 on __Type { + fragment Three on __Type { fields { - type { - name - } + name } } `); From ae49605cf3887a4b81b6eb30f152974eae2f2518 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 22 Apr 2024 16:42:48 +0200 Subject: [PATCH 18/36] same same --- src/validation/rules/MaxIntrospectionDepthRule.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 113c493203..4985dd05e8 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -5,11 +5,6 @@ import { Kind } from '../../language/kinds.js'; import type { ASTVisitor } from '../../language/visitor.js'; import { BREAK } from '../../language/visitor.js'; -import { - SchemaMetaFieldDef, - TypeMetaFieldDef, -} from '../../type/introspection.js'; - import type { ValidationContext } from '../ValidationContext.js'; const MAX_FIELDS_DEPTH = 3; @@ -52,11 +47,7 @@ export function MaxIntrospectionDepthRule( return { Field(field) { - if ( - [SchemaMetaFieldDef.name, TypeMetaFieldDef.name].includes( - field.name.value, - ) - ) { + if (['__schema', '__type'].includes(field.name.value)) { if (checkFieldsDepth(field)) { context.reportError( new GraphQLError('Maximum introspection depth exceeded'), From 3205004ab9d5b447ba9593a03e1dd169aad6f8f4 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 22 Apr 2024 17:10:48 +0200 Subject: [PATCH 19/36] no arr --- src/validation/rules/MaxIntrospectionDepthRule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 4985dd05e8..f3a592efc6 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -47,7 +47,7 @@ export function MaxIntrospectionDepthRule( return { Field(field) { - if (['__schema', '__type'].includes(field.name.value)) { + if (field.name.value === '__schema' || field.name.value === '__type') { if (checkFieldsDepth(field)) { context.reportError( new GraphQLError('Maximum introspection depth exceeded'), From 0c36d592b9d34a67a5efd250778fd468aea02cfa Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 22 Apr 2024 17:12:57 +0200 Subject: [PATCH 20/36] inline fragments --- .../MaxIntrospectionDepthRule-test.ts | 30 +++++++++++++++++++ .../rules/MaxIntrospectionDepthRule.ts | 1 + 2 files changed, 31 insertions(+) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index 47154b20ce..cdc7bca0fa 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -252,6 +252,36 @@ describe('Validate: Max introspection nodes rule', () => { `); }); + it('3 fields deep introspection query with inline fragments', () => { + expectInvalid(` + query test { + __schema { + types { + ... on __Type { + fields { + type { + ... on __Type { + ofType { + fields { + type { + ... on __Type { + fields { + name + } + } + } + } + } + } + } + } + } + } + } + } + `); + }); + it('3 fields deep introspection query with fragments', () => { expectInvalid(` query test { diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index f3a592efc6..f6f6e11512 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -34,6 +34,7 @@ export function MaxIntrospectionDepthRule( return true; } + // handles inline fragments as well if ('selectionSet' in node && node.selectionSet) { for (const child of node.selectionSet.selections) { if (checkFieldsDepth(child, depth)) { From 6892b201f0c6e19e00e9cef92c671f7e2217565d Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 22 Apr 2024 17:29:11 +0200 Subject: [PATCH 21/36] disallow fields, interfaces, possibleTypes, inputFields --- .../MaxIntrospectionDepthRule-test.ts | 60 +++++++++++++++++++ .../rules/MaxIntrospectionDepthRule.ts | 18 +++--- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index cdc7bca0fa..e65f29ece4 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -78,6 +78,66 @@ describe('Validate: Max introspection nodes rule', () => { `); }); + it('3 interfaces deep introspection query from __schema', () => { + expectInvalid(` + { + __schema { + types { + interfaces { + interfaces { + interfaces { + name + } + } + } + } + } + } + `); + }); + + it('3 possibleTypes deep introspection query from __schema', () => { + expectInvalid(` + { + __schema { + types { + possibleTypes { + possibleTypes { + possibleTypes { + name + } + } + } + } + } + } + `); + }); + + it('3 possibleTypes deep introspection query from __schema', () => { + expectInvalid(` + { + __schema { + types { + inputFields { + type { + inputFields { + type { + inputFields { + type { + name + } + } + } + } + } + } + } + } + } + `); + }); + it('3 fields deep introspection query from multiple __schema', () => { expectInvalid(` { diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index f6f6e11512..27675a9038 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -7,7 +7,7 @@ import { BREAK } from '../../language/visitor.js'; import type { ValidationContext } from '../ValidationContext.js'; -const MAX_FIELDS_DEPTH = 3; +const MAX_DEPTH = 3; export function MaxIntrospectionDepthRule( context: ValidationContext, @@ -16,20 +16,24 @@ export function MaxIntrospectionDepthRule( * Counts the depth of "__Type.fields" recursively and * returns `true` if the limit has been reached. */ - function checkFieldsDepth(node: ASTNode, depth: number = 0): boolean { + function checkDepth(node: ASTNode, depth: number = 0): boolean { if (node.kind === Kind.FRAGMENT_SPREAD) { const fragment = context.getFragment(node.name.value); if (!fragment) { throw new Error(`Fragment ${node.name.value} not found`); } - return checkFieldsDepth(fragment, depth); + return checkDepth(fragment, depth); } if ( 'name' in node && - node.name?.value === 'fields' && + // check all introspection lists + (node.name?.value === 'fields' || + node.name?.value === 'interfaces' || + node.name?.value === 'possibleTypes' || + node.name?.value === 'inputFields') && // eslint-disable-next-line no-param-reassign - ++depth >= MAX_FIELDS_DEPTH + ++depth >= MAX_DEPTH ) { return true; } @@ -37,7 +41,7 @@ export function MaxIntrospectionDepthRule( // handles inline fragments as well if ('selectionSet' in node && node.selectionSet) { for (const child of node.selectionSet.selections) { - if (checkFieldsDepth(child, depth)) { + if (checkDepth(child, depth)) { return true; } } @@ -49,7 +53,7 @@ export function MaxIntrospectionDepthRule( return { Field(field) { if (field.name.value === '__schema' || field.name.value === '__type') { - if (checkFieldsDepth(field)) { + if (checkDepth(field)) { context.reportError( new GraphQLError('Maximum introspection depth exceeded'), ); From 0398d7bf42f29f50fd186b81eafcf88f64f1ea01 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 22 Apr 2024 17:33:35 +0200 Subject: [PATCH 22/36] start from OperationDefinition and pass in node --- .../MaxIntrospectionDepthRule-test.ts | 153 ++++++++++++++---- .../rules/MaxIntrospectionDepthRule.ts | 22 ++- 2 files changed, 137 insertions(+), 38 deletions(-) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index e65f29ece4..744bb777e8 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -6,19 +6,12 @@ import { MaxIntrospectionDepthRule } from '../rules/MaxIntrospectionDepthRule.js import { expectValidationErrors } from './harness.js'; -function expectInvalid(queryStr: string) { - return expectValidationErrors( - MaxIntrospectionDepthRule, - queryStr, - ).toDeepEqual([ - { - message: 'Maximum introspection depth exceeded', - }, - ]); +function expectErrors(queryStr: string) { + return expectValidationErrors(MaxIntrospectionDepthRule, queryStr); } function expectValid(queryStr: string) { - expectValidationErrors(MaxIntrospectionDepthRule, queryStr).toDeepEqual([]); + expectErrors(queryStr).toDeepEqual([]); } describe('Validate: Max introspection nodes rule', () => { @@ -57,7 +50,7 @@ describe('Validate: Max introspection nodes rule', () => { }); it('3 fields deep introspection query from __schema', () => { - expectInvalid(` + expectErrors(` { __schema { types { @@ -75,11 +68,21 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `); + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 5, + line: 2, + }, + ], + }, + ]); }); it('3 interfaces deep introspection query from __schema', () => { - expectInvalid(` + expectErrors(` { __schema { types { @@ -93,11 +96,21 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `); + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 5, + line: 2, + }, + ], + }, + ]); }); it('3 possibleTypes deep introspection query from __schema', () => { - expectInvalid(` + expectErrors(` { __schema { types { @@ -111,11 +124,21 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `); + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 5, + line: 2, + }, + ], + }, + ]); }); it('3 possibleTypes deep introspection query from __schema', () => { - expectInvalid(` + expectErrors(` { __schema { types { @@ -135,11 +158,21 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `); + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 5, + line: 2, + }, + ], + }, + ]); }); it('3 fields deep introspection query from multiple __schema', () => { - expectInvalid(` + expectErrors(` { one: __schema { types { @@ -187,11 +220,21 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `); + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 5, + line: 2, + }, + ], + }, + ]); }); it('3 fields deep introspection query from __type', () => { - expectInvalid(` + expectErrors(` { __type(name: "Query") { types { @@ -209,11 +252,21 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `); + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 5, + line: 2, + }, + ], + }, + ]); }); it('3 fields deep introspection query from multiple __type', () => { - expectInvalid(` + expectErrors(` { one: __type(name: "Query") { types { @@ -261,7 +314,17 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `); + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 5, + line: 2, + }, + ], + }, + ]); }); it('1 fields deep with 3 fields introspection query', () => { @@ -289,7 +352,7 @@ describe('Validate: Max introspection nodes rule', () => { }); it('3 fields deep from varying parents introspection query', () => { - expectInvalid(` + expectErrors(` { __schema { types { @@ -309,11 +372,21 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `); + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 5, + line: 2, + }, + ], + }, + ]); }); it('3 fields deep introspection query with inline fragments', () => { - expectInvalid(` + expectErrors(` query test { __schema { types { @@ -339,11 +412,21 @@ describe('Validate: Max introspection nodes rule', () => { } } } - `); + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 5, + line: 2, + }, + ], + }, + ]); }); it('3 fields deep introspection query with fragments', () => { - expectInvalid(` + expectErrors(` query test { __schema { types { @@ -373,6 +456,16 @@ describe('Validate: Max introspection nodes rule', () => { name } } - `); + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 5, + line: 2, + }, + ], + }, + ]); }); }); diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 27675a9038..e299c4ca34 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -3,7 +3,6 @@ import { GraphQLError } from '../../error/GraphQLError.js'; import type { ASTNode } from '../../language/ast.js'; import { Kind } from '../../language/kinds.js'; import type { ASTVisitor } from '../../language/visitor.js'; -import { BREAK } from '../../language/visitor.js'; import type { ValidationContext } from '../ValidationContext.js'; @@ -51,13 +50,20 @@ export function MaxIntrospectionDepthRule( } return { - Field(field) { - if (field.name.value === '__schema' || field.name.value === '__type') { - if (checkDepth(field)) { - context.reportError( - new GraphQLError('Maximum introspection depth exceeded'), - ); - return BREAK; + OperationDefinition(node) { + for (const child of node.selectionSet.selections) { + if ( + 'name' in child && + (child.name.value === '__schema' || child.name.value === '__type') + ) { + if (checkDepth(node)) { + context.reportError( + new GraphQLError('Maximum introspection depth exceeded', { + nodes: [node], + }), + ); + return; + } } } }, From 62b5d730323d4d058713491e3ed0163f7bf90fb0 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 22 Apr 2024 18:03:34 +0200 Subject: [PATCH 23/36] use kind.field --- src/validation/rules/MaxIntrospectionDepthRule.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index e299c4ca34..25daf5b064 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -25,12 +25,12 @@ export function MaxIntrospectionDepthRule( } if ( - 'name' in node && + node.kind === Kind.FIELD && // check all introspection lists - (node.name?.value === 'fields' || - node.name?.value === 'interfaces' || - node.name?.value === 'possibleTypes' || - node.name?.value === 'inputFields') && + (node.name.value === 'fields' || + node.name.value === 'interfaces' || + node.name.value === 'possibleTypes' || + node.name.value === 'inputFields') && // eslint-disable-next-line no-param-reassign ++depth >= MAX_DEPTH ) { @@ -53,7 +53,7 @@ export function MaxIntrospectionDepthRule( OperationDefinition(node) { for (const child of node.selectionSet.selections) { if ( - 'name' in child && + child.kind === Kind.FIELD && (child.name.value === '__schema' || child.name.value === '__type') ) { if (checkDepth(node)) { From a61085b9856a87517299d599bedc371c569ce217 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 22 Apr 2024 18:04:01 +0200 Subject: [PATCH 24/36] better comment --- src/validation/rules/MaxIntrospectionDepthRule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 25daf5b064..a375ee1ffe 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -37,7 +37,7 @@ export function MaxIntrospectionDepthRule( return true; } - // handles inline fragments as well + // handles fields and inline fragments if ('selectionSet' in node && node.selectionSet) { for (const child of node.selectionSet.selections) { if (checkDepth(child, depth)) { From 537eda5579af22cb1b313b19d75e888eb3eafbb0 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Tue, 23 Apr 2024 16:50:39 +0200 Subject: [PATCH 25/36] opts out if fragment is missing --- .../__tests__/MaxIntrospectionDepthRule-test.ts | 12 ++++++++++++ src/validation/rules/MaxIntrospectionDepthRule.ts | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index 744bb777e8..6be44d8cd6 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -468,4 +468,16 @@ describe('Validate: Max introspection nodes rule', () => { }, ]); }); + + it('opts out if fragment is missing', () => { + expectValid(` + query test { + __schema { + types { + ...Missing + } + } + } + `); + }); }); diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index a375ee1ffe..9f9abc23e2 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -19,7 +19,8 @@ export function MaxIntrospectionDepthRule( if (node.kind === Kind.FRAGMENT_SPREAD) { const fragment = context.getFragment(node.name.value); if (!fragment) { - throw new Error(`Fragment ${node.name.value} not found`); + // missing fragments checks are handled by the `KnownFragmentNamesRule` + return false; } return checkDepth(fragment, depth); } From a2a5060efbd8e43fa7083f83492e72fa3e566327 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 29 Apr 2024 14:05:45 +0200 Subject: [PATCH 26/36] use fields and inline fragment on schema --- .../MaxIntrospectionDepthRule-test.ts | 60 ++++++++++++------- .../rules/MaxIntrospectionDepthRule.ts | 24 ++++---- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index 6be44d8cd6..c32fd99446 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -73,8 +73,8 @@ describe('Validate: Max introspection nodes rule', () => { message: 'Maximum introspection depth exceeded', locations: [ { - column: 5, - line: 2, + column: 7, + line: 3, }, ], }, @@ -101,8 +101,8 @@ describe('Validate: Max introspection nodes rule', () => { message: 'Maximum introspection depth exceeded', locations: [ { - column: 5, - line: 2, + column: 7, + line: 3, }, ], }, @@ -129,8 +129,8 @@ describe('Validate: Max introspection nodes rule', () => { message: 'Maximum introspection depth exceeded', locations: [ { - column: 5, - line: 2, + column: 7, + line: 3, }, ], }, @@ -163,8 +163,8 @@ describe('Validate: Max introspection nodes rule', () => { message: 'Maximum introspection depth exceeded', locations: [ { - column: 5, - line: 2, + column: 7, + line: 3, }, ], }, @@ -225,8 +225,8 @@ describe('Validate: Max introspection nodes rule', () => { message: 'Maximum introspection depth exceeded', locations: [ { - column: 5, - line: 2, + column: 7, + line: 3, }, ], }, @@ -257,8 +257,8 @@ describe('Validate: Max introspection nodes rule', () => { message: 'Maximum introspection depth exceeded', locations: [ { - column: 5, - line: 2, + column: 7, + line: 3, }, ], }, @@ -319,8 +319,8 @@ describe('Validate: Max introspection nodes rule', () => { message: 'Maximum introspection depth exceeded', locations: [ { - column: 5, - line: 2, + column: 7, + line: 3, }, ], }, @@ -377,8 +377,8 @@ describe('Validate: Max introspection nodes rule', () => { message: 'Maximum introspection depth exceeded', locations: [ { - column: 5, - line: 2, + column: 7, + line: 3, }, ], }, @@ -417,8 +417,8 @@ describe('Validate: Max introspection nodes rule', () => { message: 'Maximum introspection depth exceeded', locations: [ { - column: 5, - line: 2, + column: 7, + line: 3, }, ], }, @@ -461,8 +461,28 @@ describe('Validate: Max introspection nodes rule', () => { message: 'Maximum introspection depth exceeded', locations: [ { - column: 5, - line: 2, + column: 7, + line: 3, + }, + ], + }, + ]); + }); + + it('3 fields deep inside inline fragment on query', () => { + expectErrors(` + { + ... { + __schema { types { fields { type { fields { type { fields { name } } } } } } } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 9, + line: 4, }, ], }, diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 9f9abc23e2..6adc3cb140 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -3,6 +3,7 @@ import { GraphQLError } from '../../error/GraphQLError.js'; import type { ASTNode } from '../../language/ast.js'; import { Kind } from '../../language/kinds.js'; import type { ASTVisitor } from '../../language/visitor.js'; +import { BREAK } from '../../language/visitor.js'; import type { ValidationContext } from '../ValidationContext.js'; @@ -51,20 +52,15 @@ export function MaxIntrospectionDepthRule( } return { - OperationDefinition(node) { - for (const child of node.selectionSet.selections) { - if ( - child.kind === Kind.FIELD && - (child.name.value === '__schema' || child.name.value === '__type') - ) { - if (checkDepth(node)) { - context.reportError( - new GraphQLError('Maximum introspection depth exceeded', { - nodes: [node], - }), - ); - return; - } + Field(node) { + if (node.name.value === '__schema' || node.name.value === '__type') { + if (checkDepth(node)) { + context.reportError( + new GraphQLError('Maximum introspection depth exceeded', { + nodes: [node], + }), + ); + return BREAK; } } }, From 4ba281e71f5e1bcde8252116824be1fa3f01ae48 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 29 Apr 2024 15:14:18 +0200 Subject: [PATCH 27/36] we're counting lists --- src/validation/rules/MaxIntrospectionDepthRule.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 6adc3cb140..f30d6260cd 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -7,13 +7,13 @@ import { BREAK } from '../../language/visitor.js'; import type { ValidationContext } from '../ValidationContext.js'; -const MAX_DEPTH = 3; +const MAX_LISTS_DEPTH = 3; export function MaxIntrospectionDepthRule( context: ValidationContext, ): ASTVisitor { /** - * Counts the depth of "__Type.fields" recursively and + * Counts the depth of list fields in "__Type" recursively and * returns `true` if the limit has been reached. */ function checkDepth(node: ASTNode, depth: number = 0): boolean { @@ -34,7 +34,7 @@ export function MaxIntrospectionDepthRule( node.name.value === 'possibleTypes' || node.name.value === 'inputFields') && // eslint-disable-next-line no-param-reassign - ++depth >= MAX_DEPTH + ++depth >= MAX_LISTS_DEPTH ) { return true; } From 1fa81c05e4596291b800d585e0d35366dd98e782 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 20 May 2024 16:58:35 +0200 Subject: [PATCH 28/36] less clever --- src/validation/rules/MaxIntrospectionDepthRule.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index f30d6260cd..4267eea06f 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -32,11 +32,13 @@ export function MaxIntrospectionDepthRule( (node.name.value === 'fields' || node.name.value === 'interfaces' || node.name.value === 'possibleTypes' || - node.name.value === 'inputFields') && - // eslint-disable-next-line no-param-reassign - ++depth >= MAX_LISTS_DEPTH + node.name.value === 'inputFields') ) { - return true; + // eslint-disable-next-line no-param-reassign + depth++; + if (depth >= MAX_LISTS_DEPTH) { + return true; + } } // handles fields and inline fragments From 572b51f8eca2cc3aad35bfaa9cd71d98dd09dbb7 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 20 May 2024 17:02:22 +0200 Subject: [PATCH 29/36] dont break, just dont visit further --- .../MaxIntrospectionDepthRule-test.ts | 36 +++++++++++++++++++ .../rules/MaxIntrospectionDepthRule.ts | 3 +- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index c32fd99446..35eccf342f 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -230,6 +230,24 @@ describe('Validate: Max introspection nodes rule', () => { }, ], }, + { + locations: [ + { + column: 7, + line: 18, + }, + ], + message: 'Maximum introspection depth exceeded', + }, + { + locations: [ + { + column: 7, + line: 33, + }, + ], + message: 'Maximum introspection depth exceeded', + }, ]); }); @@ -324,6 +342,24 @@ describe('Validate: Max introspection nodes rule', () => { }, ], }, + { + locations: [ + { + column: 7, + line: 18, + }, + ], + message: 'Maximum introspection depth exceeded', + }, + { + locations: [ + { + column: 7, + line: 33, + }, + ], + message: 'Maximum introspection depth exceeded', + }, ]); }); diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 4267eea06f..8ab7715126 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -3,7 +3,6 @@ import { GraphQLError } from '../../error/GraphQLError.js'; import type { ASTNode } from '../../language/ast.js'; import { Kind } from '../../language/kinds.js'; import type { ASTVisitor } from '../../language/visitor.js'; -import { BREAK } from '../../language/visitor.js'; import type { ValidationContext } from '../ValidationContext.js'; @@ -62,7 +61,7 @@ export function MaxIntrospectionDepthRule( nodes: [node], }), ); - return BREAK; + return false; } } }, From 95fcced2e69dd28ee5c2cfe48f536e5e3b9779e0 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 20 May 2024 17:40:31 +0200 Subject: [PATCH 30/36] 3 input fields --- src/validation/__tests__/MaxIntrospectionDepthRule-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index 35eccf342f..d56b321669 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -137,7 +137,7 @@ describe('Validate: Max introspection nodes rule', () => { ]); }); - it('3 possibleTypes deep introspection query from __schema', () => { + it('3 inputFields deep introspection query from __schema', () => { expectErrors(` { __schema { From 77767b8151ce94f31a40223c649ddd67a6e67d00 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 3 Jun 2024 13:01:31 +0200 Subject: [PATCH 31/36] doesn't infinitely recurse on fragment cycle --- .../MaxIntrospectionDepthRule-test.ts | 15 +++++++++ .../rules/MaxIntrospectionDepthRule.ts | 32 ++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts index d56b321669..ff6826c873 100644 --- a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -536,4 +536,19 @@ describe('Validate: Max introspection nodes rule', () => { } `); }); + + it("doesn't infinitely recurse on fragment cycle", () => { + expectValid(` + query test { + __schema { + types { + ...Cycle + } + } + } + fragment Cycle on __Type { + ...Cycle + } + `); + }); }); diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 8ab7715126..8fb2f1d1de 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -15,14 +15,36 @@ export function MaxIntrospectionDepthRule( * Counts the depth of list fields in "__Type" recursively and * returns `true` if the limit has been reached. */ - function checkDepth(node: ASTNode, depth: number = 0): boolean { + function checkDepth( + node: ASTNode, + visitedFragments: { + [fragmentName: string]: true | undefined; + } = Object.create(null), + depth: number = 0, + ): boolean { if (node.kind === Kind.FRAGMENT_SPREAD) { - const fragment = context.getFragment(node.name.value); + const fragmentName = node.name.value; + if (visitedFragments[fragmentName]) { + // Fragment cycles are handled by `NoFragmentCyclesRule`. + return false; + } + const fragment = context.getFragment(fragmentName); if (!fragment) { - // missing fragments checks are handled by the `KnownFragmentNamesRule` + // Missing fragments checks are handled by the `KnownFragmentNamesRule`. return false; } - return checkDepth(fragment, depth); + + // Rather than following an immutable programming pattern which has + // significant memory and garbage collection overhead, we've opted to + // take a mutable approach for efficiency's sake. Importantly visiting a + // fragment twice is fine, so long as you don't do one visit inside the + // other. + try { + visitedFragments[fragmentName] = true; + return checkDepth(fragment, visitedFragments, depth); + } finally { + visitedFragments[fragmentName] = undefined; + } } if ( @@ -43,7 +65,7 @@ export function MaxIntrospectionDepthRule( // handles fields and inline fragments if ('selectionSet' in node && node.selectionSet) { for (const child of node.selectionSet.selections) { - if (checkDepth(child, depth)) { + if (checkDepth(child, visitedFragments, depth)) { return true; } } From 7f16076e7eb4d0b1598546573ac0bc3468c7c1ec Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 3 Jun 2024 13:09:24 +0200 Subject: [PATCH 32/36] reorder --- src/validation/specifiedRules.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/specifiedRules.ts b/src/validation/specifiedRules.ts index e88a39deef..2d380bcdf5 100644 --- a/src/validation/specifiedRules.ts +++ b/src/validation/specifiedRules.ts @@ -23,9 +23,9 @@ import { KnownFragmentNamesRule } from './rules/KnownFragmentNamesRule.js'; import { KnownTypeNamesRule } from './rules/KnownTypeNamesRule.js'; // Spec Section: "Lone Anonymous Operation" import { LoneAnonymousOperationRule } from './rules/LoneAnonymousOperationRule.js'; +import { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; // SDL-specific validation rules import { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.js'; -import { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; // Spec Section: "Fragments must not form cycles" import { NoFragmentCyclesRule } from './rules/NoFragmentCyclesRule.js'; // Spec Section: "All Variable Used Defined" From d1113cc33f4707f1452830b8a8fbaf363624a2c0 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 3 Jun 2024 13:16:20 +0200 Subject: [PATCH 33/36] === true --- src/validation/rules/MaxIntrospectionDepthRule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 8fb2f1d1de..e24427d4a4 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -24,7 +24,7 @@ export function MaxIntrospectionDepthRule( ): boolean { if (node.kind === Kind.FRAGMENT_SPREAD) { const fragmentName = node.name.value; - if (visitedFragments[fragmentName]) { + if (visitedFragments[fragmentName] === true) { // Fragment cycles are handled by `NoFragmentCyclesRule`. return false; } From b15fbcd7634b58ef0d27f473836c6704e83eddb3 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 3 Jun 2024 16:58:59 +0200 Subject: [PATCH 34/36] leave todo --- src/validation/rules/MaxIntrospectionDepthRule.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index e24427d4a4..5cde22f130 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -50,6 +50,7 @@ export function MaxIntrospectionDepthRule( if ( node.kind === Kind.FIELD && // check all introspection lists + // TODO: instead of relying on field names, check whether the type is a list (node.name.value === 'fields' || node.name.value === 'interfaces' || node.name.value === 'possibleTypes' || From 0eb5b88aed6331474ccd9e109d555d6542bfc8c4 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 19 Jun 2024 11:49:57 +0200 Subject: [PATCH 35/36] comment --- src/validation/specifiedRules.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/validation/specifiedRules.ts b/src/validation/specifiedRules.ts index 2d380bcdf5..d55d794918 100644 --- a/src/validation/specifiedRules.ts +++ b/src/validation/specifiedRules.ts @@ -23,7 +23,6 @@ import { KnownFragmentNamesRule } from './rules/KnownFragmentNamesRule.js'; import { KnownTypeNamesRule } from './rules/KnownTypeNamesRule.js'; // Spec Section: "Lone Anonymous Operation" import { LoneAnonymousOperationRule } from './rules/LoneAnonymousOperationRule.js'; -import { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; // SDL-specific validation rules import { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.js'; // Spec Section: "Fragments must not form cycles" @@ -76,6 +75,9 @@ import { VariablesAreInputTypesRule } from './rules/VariablesAreInputTypesRule.j import { VariablesInAllowedPositionRule } from './rules/VariablesInAllowedPositionRule.js'; import type { SDLValidationRule, ValidationRule } from './ValidationContext.js'; +// TODO: Spec Section +import { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; + /** * This set includes all validation rules defined by the GraphQL spec. * @@ -113,6 +115,7 @@ export const specifiedRules: ReadonlyArray = Object.freeze([ VariablesInAllowedPositionRule, OverlappingFieldsCanBeMergedRule, UniqueInputFieldNamesRule, + // Technically this isn't part of the spec but it's a strongly encouraged validation rule. MaxIntrospectionDepthRule, ]); From bb122de6bfda4c0acef70668e72397dc4b114de8 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 21 Jun 2024 10:03:21 +0100 Subject: [PATCH 36/36] Move recommended rules to their own list --- src/index.ts | 1 + src/validation/index.ts | 2 +- src/validation/specifiedRules.ts | 12 ++++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index c90a1a5fc9..fb67b4c75e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -355,6 +355,7 @@ export { ValidationContext, // All validation rules in the GraphQL Specification. specifiedRules, + recommendedRules, // Individual validation rules. ExecutableDefinitionsRule, FieldsOnCorrectTypeRule, diff --git a/src/validation/index.ts b/src/validation/index.ts index fae63df69b..dbe8e57dc0 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -4,7 +4,7 @@ export { ValidationContext } from './ValidationContext.js'; export type { ValidationRule } from './ValidationContext.js'; // All validation rules in the GraphQL Specification. -export { specifiedRules } from './specifiedRules.js'; +export { specifiedRules, recommendedRules } from './specifiedRules.js'; // Spec Section: "Defer And Stream Directive Labels Are Unique" export { DeferStreamDirectiveLabelRule } from './rules/DeferStreamDirectiveLabelRule.js'; diff --git a/src/validation/specifiedRules.ts b/src/validation/specifiedRules.ts index d55d794918..9884bea8b9 100644 --- a/src/validation/specifiedRules.ts +++ b/src/validation/specifiedRules.ts @@ -25,6 +25,8 @@ import { KnownTypeNamesRule } from './rules/KnownTypeNamesRule.js'; import { LoneAnonymousOperationRule } from './rules/LoneAnonymousOperationRule.js'; // SDL-specific validation rules import { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.js'; +// TODO: Spec Section +import { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; // Spec Section: "Fragments must not form cycles" import { NoFragmentCyclesRule } from './rules/NoFragmentCyclesRule.js'; // Spec Section: "All Variable Used Defined" @@ -75,8 +77,11 @@ import { VariablesAreInputTypesRule } from './rules/VariablesAreInputTypesRule.j import { VariablesInAllowedPositionRule } from './rules/VariablesInAllowedPositionRule.js'; import type { SDLValidationRule, ValidationRule } from './ValidationContext.js'; -// TODO: Spec Section -import { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.js'; +/** + * Technically these aren't part of the spec but they are strongly encouraged + * validation rules. + */ +export const recommendedRules = Object.freeze([MaxIntrospectionDepthRule]); /** * This set includes all validation rules defined by the GraphQL spec. @@ -115,8 +120,7 @@ export const specifiedRules: ReadonlyArray = Object.freeze([ VariablesInAllowedPositionRule, OverlappingFieldsCanBeMergedRule, UniqueInputFieldNamesRule, - // Technically this isn't part of the spec but it's a strongly encouraged validation rule. - MaxIntrospectionDepthRule, + ...recommendedRules, ]); /**