CVE-2024-34709: Insufficient Information (NVD-CWE-noinfo)
value: Directus is a real-time API and App dashboard for managing SQL database content. Prior to 10.11.0, session tokens function like the other JWT tokens where they are not actually invalidated when logging out. The `directus_session` gets destroyed and the cookie gets deleted but if the cookie value is captured, it will still work for the entire expiry time which is set to 1 day by default. Making it effectively a long lived unrevokable stateless token instead of the stateful session token it was meant to be. This vulnerability is fixed in 10.11.0.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-14T15:39Z
Repository: https://github.com/directus/directus
Language: TypeScript
Files changed: .changeset/late-lions-pump.md, api/src/utils/get-accountability-for-token.ts, api/src/utils/verify-session-jwt.ts
Commit date: 2024-05-02 14:53:41 +0200
Commit message: Improved session token validation (#22353)

Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
Diff: diff --git a/.changeset/late-lions-pump.md b/.changeset/late-lions-pump.md
new file mode 100644
index 0000000000..192eebb481
--- /dev/null
+++ b/.changeset/late-lions-pump.md
@@ -0,0 +1,5 @@
+---
+"@directus/api": patch
+---
+
+Improved session token validation
diff --git a/api/src/utils/get-accountability-for-token.ts b/api/src/utils/get-accountability-for-token.ts
index a81f59623a..d4a4800107 100644
--- a/api/src/utils/get-accountability-for-token.ts
+++ b/api/src/utils/get-accountability-for-token.ts
@@ -1,10 +1,11 @@
 import { useEnv } from '@directus/env';
 import { InvalidCredentialsError } from '@directus/errors';
 import type { Accountability } from '@directus/types';
 import getDatabase from '../database/index.js';
 import isDirectusJWT from './is-directus-jwt.js';
+import { verifySessionJWT } from './verify-session-jwt.js';
 import { verifyAccessJWT } from './jwt.js';
 
 export async function getAccountabilityForToken(
 	token?: string | null,
 	accountability?: Accountability,
@@ -22,10 +23,14 @@ export async function getAccountabilityForToken(
 
 	if (token) {
 		if (isDirectusJWT(token)) {
 			const payload = verifyAccessJWT(token, env['SECRET'] as string);
 
+			if ('session' in payload) {
+				await verifySessionJWT(payload);
+			}
+
 			accountability.role = payload.role;
 			accountability.admin = payload.admin_access === true || payload.admin_access == 1;
 			accountability.app = payload.app_access === true || payload.app_access == 1;
 
 			if (payload.share) accountability.share = payload.share;
diff --git a/api/src/utils/verify-session-jwt.ts b/api/src/utils/verify-session-jwt.ts
new file mode 100644
index 0000000000..3a6bdeef2b
--- /dev/null
+++ b/api/src/utils/verify-session-jwt.ts
@@ -0,0 +1,26 @@
+import getDatabase from '../database/index.js';
+import { InvalidTokenError } from '@directus/errors';
+import type { DirectusTokenPayload } from '../types/index.js';
+
+/**
+ * Verifies the associated session is still available and valid.
+ *
+ * @throws If session not found.
+ */
+export async function verifySessionJWT(payload: DirectusTokenPayload) {
+	const database = getDatabase();
+
+	const session = await database
+		.select(1)
+		.from('directus_sessions')
+		.where({
+			token: payload['session'],
+			user: payload['id'],
+		})
+		.andWhere('expires', '>=', new Date())
+		.first();
+
+	if (!session) {
+		throw new InvalidTokenError();
+	}
+}

Fixed code: ---
"@directus/api": patch
---

Improved session token validation
import { verifySessionJWT } from './verify-session-jwt.js';
			if ('session' in payload) {
				await verifySessionJWT(payload);
			}

import getDatabase from '../database/index.js';
import { InvalidTokenError } from '@directus/errors';
import type { DirectusTokenPayload } from '../types/index.js';

/**
 * Verifies the associated session is still available and valid.
 *
 * @throws If session not found.
 */
export async function verifySessionJWT(payload: DirectusTokenPayload) {
	const database = getDatabase();

	const session = await database
		.select(1)
		.from('directus_sessions')
		.where({
			token: payload['session'],
			user: payload['id'],
		})
		.andWhere('expires', '>=', new Date())
		.first();

	if (!session) {
		throw new InvalidTokenError();
	}
}