CVE-2024-39895: Insufficient Information (NVD-CWE-noinfo)
value: Directus is a real-time API and App dashboard for managing SQL database content. A denial of service (DoS) attack by field duplication in GraphQL is a type of attack where an attacker exploits the flexibility of GraphQL to overwhelm a server by requesting the same field multiple times in a single query. This can cause the server to perform redundant computations and consume excessive resources, leading to a denial of service for legitimate users. Request to the endpoint /graphql are sent when visualizing graphs generated at a dashboard. By modifying the data sent and duplicating many times the fields a DoS attack is possible. This vulnerability is fixed in 10.12.0.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-07-08T17:15Z
Repository: https://github.com/directus/directus
Language: TypeScript
Files changed: .changeset/flat-kids-jump.md, api/src/middleware/graphql.ts, docs/self-hosted/config-options.md, packages/env/src/constants/defaults.ts, packages/env/src/constants/directus-variables.ts
Commit date: 2024-06-04 15:46:50 +0200
Commit message: Merge pull request from GHSA-7hmh-pfrp-vcx4

* Add gql parser limit

* Add changeset

* Format docs

---------

Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
Diff: diff --git a/.changeset/flat-kids-jump.md b/.changeset/flat-kids-jump.md
new file mode 100644
index 00000000000..6054e27b952
--- /dev/null
+++ b/.changeset/flat-kids-jump.md
@@ -0,0 +1,6 @@
+---
+'@directus/env': patch
+'@directus/api': patch
+---
+
+Added a configurable max token limit for GraphQL queries
diff --git a/api/src/middleware/graphql.ts b/api/src/middleware/graphql.ts
index 44a4ba02749..9ce30eb43f9 100644
--- a/api/src/middleware/graphql.ts
+++ b/api/src/middleware/graphql.ts
@@ -4,10 +4,11 @@ import type { DocumentNode } from 'graphql';
 import { getOperationAST, parse, Source } from 'graphql';
 import { InvalidPayloadError, InvalidQueryError, MethodNotAllowedError } from '@directus/errors';
 import { GraphQLValidationError } from '../services/graphql/errors/validation.js';
 import type { GraphQLParams } from '../types/index.js';
 import asyncHandler from '../utils/async-handler.js';
+import { useEnv } from '@directus/env';
 
 export const parseGraphQL: RequestHandler = asyncHandler(async (req, res, next) => {
 	if (req.method !== 'GET' && req.method !== 'POST') {
 		throw new MethodNotAllowedError({ allowed: ['GET', 'POST'], current: req.method });
 	}
@@ -40,11 +41,15 @@ export const parseGraphQL: RequestHandler = asyncHandler(async (req, res, next)
 	if (query === null) {
 		throw new InvalidPayloadError({ reason: 'Must provide query string' });
 	}
 
 	try {
-		document = parse(new Source(query));
+		const env = useEnv();
+
+		document = parse(new Source(query), {
+			maxTokens: Number(env['GRAPHQL_QUERY_TOKEN_LIMIT']),
+		});
 	} catch (err: any) {
 		throw new GraphQLValidationError({
 			errors: [err],
 		});
 	}
diff --git a/docs/self-hosted/config-options.md b/docs/self-hosted/config-options.md
index 0b65294db94..830adc57d68 100644
--- a/docs/self-hosted/config-options.md
+++ b/docs/self-hosted/config-options.md
@@ -1045,14 +1045,15 @@ Directus collects little and anonymized data about your environment.
 
 ## Limits & Optimizations
 
 Allows you to configure hard technical limits, to prevent abuse and optimize for your particular server environment.
 
-| Variable                | Description                                                                               | Default Value |
-| ----------------------- | ----------------------------------------------------------------------------------------- | ------------- |
-| `RELATIONAL_BATCH_SIZE` | How many rows are read into memory at a time when constructing nested relational datasets | 25000         |
-| `EXPORT_BATCH_SIZE`     | How many rows are read into memory at a time when constructing exports                    | 5000          |
+| Variable                    | Description                                                                                                                     | Default Value |
+| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------- |
+| `RELATIONAL_BATCH_SIZE`     | How many rows are read into memory at a time when constructing nested relational datasets                                       | 25000         |
+| `EXPORT_BATCH_SIZE`         | How many rows are read into memory at a time when constructing exports                                                          | 5000          |
+| `GRAPHQL_QUERY_TOKEN_LIMIT` | How many GraphQL query tokens will be parsed. [More details here](https://graphql-js.org/api/interface/parseoptions/#maxTokens) | 5000          |
 
 ## WebSockets
 
 | Variable                                    | Description                                                                                                                      | Default Value |
 | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------- |
diff --git a/packages/env/src/constants/defaults.ts b/packages/env/src/constants/defaults.ts
index 1a1b4509582..d98af6f80fa 100644
--- a/packages/env/src/constants/defaults.ts
+++ b/packages/env/src/constants/defaults.ts
@@ -118,10 +118,11 @@ export const DEFAULTS = {
 	EXPORT_BATCH_SIZE: 5000,
 
 	FILE_METADATA_ALLOW_LIST: 'ifd0.Make,ifd0.Model,exif.FNumber,exif.ExposureTime,exif.FocalLength,exif.ISOSpeedRatings',
 
 	GRAPHQL_INTROSPECTION: true,
+	GRAPHQL_QUERY_TOKEN_LIMIT: 5000,
 
 	WEBSOCKETS_ENABLED: false,
 	WEBSOCKETS_REST_ENABLED: true,
 	WEBSOCKETS_REST_AUTH: 'handshake',
 	WEBSOCKETS_REST_AUTH_TIMEOUT: 10,
diff --git a/packages/env/src/constants/directus-variables.ts b/packages/env/src/constants/directus-variables.ts
index a710ac5829a..7e67f1d417d 100644
--- a/packages/env/src/constants/directus-variables.ts
+++ b/packages/env/src/constants/directus-variables.ts
@@ -13,10 +13,11 @@ export const DIRECTUS_VARIABLES = [
 	'MAX_PAYLOAD_SIZE',
 	'ROOT_REDIRECT',
 	'SERVE_APP',
 	'GRAPHQL_INTROSPECTION',
 	'GRAPHQL_SCHEMA_CACHE_CAPACITY',
+	'GRAPHQL_QUERY_TOKEN_LIMIT',
 	'MAX_BATCH_MUTATION',
 	'LOGGER_.+',
 	'QUERY_LIMIT_MAX',
 	'QUERY_LIMIT_DEFAULT',
 	'ROBOTS_TXT',

Vulnerable code: 		document = parse(new Source(query));
| Variable                | Description                                                                               | Default Value |
| ----------------------- | ----------------------------------------------------------------------------------------- | ------------- |
| `RELATIONAL_BATCH_SIZE` | How many rows are read into memory at a time when constructing nested relational datasets | 25000         |
| `EXPORT_BATCH_SIZE`     | How many rows are read into memory at a time when constructing exports                    | 5000          |
Fixed code: ---
'@directus/env': patch
'@directus/api': patch
---

Added a configurable max token limit for GraphQL queries
import { useEnv } from '@directus/env';
		const env = useEnv();

		document = parse(new Source(query), {
			maxTokens: Number(env['GRAPHQL_QUERY_TOKEN_LIMIT']),
		});
| Variable                    | Description                                                                                                                     | Default Value |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `RELATIONAL_BATCH_SIZE`     | How many rows are read into memory at a time when constructing nested relational datasets                                       | 25000         |
| `EXPORT_BATCH_SIZE`         | How many rows are read into memory at a time when constructing exports                                                          | 5000          |
| `GRAPHQL_QUERY_TOKEN_LIMIT` | How many GraphQL query tokens will be parsed. [More details here](https://graphql-js.org/api/interface/parseoptions/#maxTokens) | 5000          |
	GRAPHQL_QUERY_TOKEN_LIMIT: 5000,
	'GRAPHQL_QUERY_TOKEN_LIMIT',