Security - Add complexity max on gql queries (#16069)
closes https://github.com/twentyhq/private-issues/issues/348 closes https://github.com/twentyhq/private-issues/issues/352 closes https://github.com/twentyhq/private-issues/issues/353 closes https://github.com/twentyhq/private-issues/issues/354
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
||||
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { useSentryTracing } from 'src/engine/core-modules/exception-handler/hooks/use-sentry-tracing';
|
||||
import { useComputeComplexity } from 'src/engine/core-modules/graphql/hooks/use-compute-complexity.hook';
|
||||
import { useDisableIntrospectionForUnauthenticatedUsers } from 'src/engine/core-modules/graphql/hooks/use-disable-introspection-for-unauthenticated-users.hook';
|
||||
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
@@ -75,6 +76,9 @@ export class GraphQLConfigService
|
||||
useDisableIntrospectionForUnauthenticatedUsers(
|
||||
this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.PRODUCTION,
|
||||
),
|
||||
useComputeComplexity(
|
||||
this.twentyConfigService.get('GRAPHQL_MAX_COMPLEXITY'),
|
||||
),
|
||||
];
|
||||
|
||||
if (Sentry.isInitialized()) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useCachedMetadata } from 'src/engine/api/graphql/graphql-config/hooks/u
|
||||
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
|
||||
import { type CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
||||
import { type ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { useComputeComplexity } from 'src/engine/core-modules/graphql/hooks/use-compute-complexity.hook';
|
||||
import { useDisableIntrospectionForUnauthenticatedUsers } from 'src/engine/core-modules/graphql/hooks/use-disable-introspection-for-unauthenticated-users.hook';
|
||||
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
|
||||
import { type I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
@@ -45,6 +46,7 @@ export const metadataModuleFactory = async (
|
||||
useDisableIntrospectionForUnauthenticatedUsers(
|
||||
twentyConfigService.get('NODE_ENV') === NodeEnvironment.PRODUCTION,
|
||||
),
|
||||
useComputeComplexity(twentyConfigService.get('GRAPHQL_MAX_COMPLEXITY')),
|
||||
],
|
||||
path: '/metadata',
|
||||
context: () => ({
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type ValidationContext } from 'graphql';
|
||||
import { type Plugin } from 'graphql-yoga';
|
||||
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
export const useComputeComplexity = (maximumComplexity: number): Plugin => ({
|
||||
onValidate: ({ addValidationRule }) => {
|
||||
addValidationRule((context: ValidationContext) => {
|
||||
let complexity = 0;
|
||||
|
||||
return {
|
||||
Field() {
|
||||
complexity++;
|
||||
},
|
||||
Document: {
|
||||
leave() {
|
||||
if (complexity > maximumComplexity) {
|
||||
context.reportError(
|
||||
new UserInputError(
|
||||
`Query complexity is too high: ${complexity} - Too many fields requested`,
|
||||
{
|
||||
userFriendlyMessage: msg`The request is too complex to process. Please try reducing the amount of data requested.`,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -986,6 +986,14 @@ export class ConfigVariables {
|
||||
@CastToPositiveNumber()
|
||||
API_RATE_LIMITING_LONG_LIMIT = 100;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.RATE_LIMITING,
|
||||
description: 'Maximum complexity allowed for GQL queries',
|
||||
type: ConfigVariableType.NUMBER,
|
||||
})
|
||||
GRAPHQL_MAX_COMPLEXITY = 2000;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.SSL,
|
||||
description: 'Path to the SSL key for enabling HTTPS in local development',
|
||||
|
||||
+2
-2
@@ -24,9 +24,9 @@ import {
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
FieldMetadataSettings,
|
||||
FieldMetadataOptions,
|
||||
FieldMetadataSettings,
|
||||
FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export const generateGqlFields = (count: number): string => {
|
||||
return Array.from({ length: count }, (_) => `id`).join('\n');
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { generateGqlFields } from 'test/integration/graphql/suites/query-complexity/generate-gql-fields.util';
|
||||
import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
|
||||
describe('Query Complexity', () => {
|
||||
it('should execute a simple query', async () => {
|
||||
const gqlFields = generateGqlFields(100);
|
||||
|
||||
const findManyPeopleOperation = findManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(findManyPeopleOperation);
|
||||
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.people).toBeDefined();
|
||||
expect(response.body.data.people.edges).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail to execute a query with too many fields', async () => {
|
||||
const gqlFields = generateGqlFields(2001);
|
||||
|
||||
const findManyPeopleOperation = findManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(findManyPeopleOperation);
|
||||
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toContain(
|
||||
'Query complexity is too high',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user