VYPR
Low severityNVD Advisory· Published Mar 26, 2025· Updated Jun 9, 2025

Suspended Directus user can continue to use session token to access API

CVE-2025-30351

Description

Directus is a real-time API and App dashboard for managing SQL database content. Starting in version 10.10.0 and prior to version 11.5.0, a suspended user can use the token generated in session auth mode to access the API despite their status. This happens because there is a check missing in verifySessionJWT to verify that a user is actually still active and allowed to access the API. One can extract the session token obtained by, e.g. login in to the app while still active and then, after the user has been suspended continue to use that token until it expires. Version 11.5.0 patches the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
directusnpm
>= 10.10.0, < 11.5.011.5.0
@directus/apinpm
>= 18.0.0, < 24.0.124.0.1
@directus/typesnpm
>= 11.0.7, < 13.0.013.0.0

Affected products

1

Patches

1
ef179931c55b

Merge from fork (#24714)

https://github.com/directus/directusBrainslugFeb 26, 2025via ghsa
4 files changed · +38 0
  • api/src/services/users.test.ts+13 0 modified
    @@ -89,6 +89,10 @@ describe('Integration Tests', () => {
     			.spyOn(UsersService.prototype as any, 'checkPasswordPolicy')
     			.mockResolvedValue(() => vi.fn());
     
    +		const clearUserSessionsSpy = vi
    +			.spyOn(UsersService.prototype as any, 'clearUserSessions')
    +			.mockResolvedValue(() => vi.fn());
    +
     		afterEach(() => {
     			vi.clearAllMocks();
     		});
    @@ -170,6 +174,7 @@ describe('Integration Tests', () => {
     				await service.updateMany([randomUUID()], {}, opts);
     
     				expect(opts.userIntegrityCheckFlags).toBe(undefined);
    +				expect(clearUserSessionsSpy).not.toBeCalled();
     			});
     
     			it('should request all user integrity checks if role is changed', async () => {
    @@ -186,6 +191,7 @@ describe('Integration Tests', () => {
     				await service.updateMany([randomUUID()], { status: 'inactive' }, opts);
     
     				expect(opts.userIntegrityCheckFlags).toBe(UserIntegrityCheckFlag.All);
    +				expect(clearUserSessionsSpy).toBeCalled();
     			});
     
     			it('should request user limit checks if status is changed to "active"', async () => {
    @@ -194,6 +200,7 @@ describe('Integration Tests', () => {
     				await service.updateMany([randomUUID()], { status: 'active' }, opts);
     
     				expect(opts.userIntegrityCheckFlags).toBe(UserIntegrityCheckFlag.UserLimits);
    +				expect(clearUserSessionsSpy).not.toBeCalled();
     			});
     
     			it('should clear caches if role is changed', async () => {
    @@ -214,6 +221,7 @@ describe('Integration Tests', () => {
     				await service.updateMany([randomUUID()], { email: 'test@example.com' });
     
     				expect(checkUniqueEmailsSpy).toBeCalledTimes(1);
    +				expect(clearUserSessionsSpy).toBeCalled();
     			});
     
     			it('should disallow updating multiple items to same email', async () => {
    @@ -227,18 +235,22 @@ describe('Integration Tests', () => {
     						field: 'email',
     					}),
     				);
    +
    +				expect(clearUserSessionsSpy).toBeCalled();
     			});
     
     			it('should not checkPasswordPolicy', async () => {
     				await service.updateMany([randomUUID()], {});
     
     				expect(checkPasswordPolicySpy).not.toBeCalled();
    +				expect(clearUserSessionsSpy).not.toBeCalled();
     			});
     
     			it('should checkPasswordPolicy once', async () => {
     				await service.updateMany([randomUUID()], { password: 'testpassword' });
     
     				expect(checkPasswordPolicySpy).toBeCalledTimes(1);
    +				expect(clearUserSessionsSpy).toBeCalled();
     			});
     
     			describe('restricted auth fields', () => {
    @@ -305,6 +317,7 @@ describe('Integration Tests', () => {
     				await service.deleteMany([randomUUID()]);
     
     				expect(validateRemainingAdminUsers).toHaveBeenCalled();
    +				expect(clearUserSessionsSpy).toBeCalled();
     			});
     		});
     
    
  • api/src/services/users.ts+23 0 modified
    @@ -107,6 +107,21 @@ export class UsersService extends ItemsService {
     		}
     	}
     
    +	/**
    +	 * Clear users' sessions to log them out
    +	 */
    +	private async clearUserSessions(userKeys: PrimaryKey[], excludeSession?: string): Promise<void> {
    +		if (excludeSession) {
    +			await this.knex
    +				.from('directus_sessions')
    +				.whereIn('user', userKeys)
    +				.andWhereNot('token', '=', excludeSession)
    +				.delete();
    +		} else {
    +			await this.knex.from('directus_sessions').whereIn('user', userKeys).delete();
    +		}
    +	}
    +
     	/**
     	 * Get basic information of user identified by email
     	 */
    @@ -292,6 +307,12 @@ export class UsersService extends ItemsService {
     
     		const result = await super.updateMany(keys, data, opts);
     
    +		if (data['status'] !== undefined && data['status'] !== 'active') {
    +			await this.clearUserSessions(keys);
    +		} else if (data['password'] !== undefined || data['email'] !== undefined) {
    +			await this.clearUserSessions(keys, this.accountability?.session);
    +		}
    +
     		// Only clear the caches if the role has been updated
     		if ('role' in data) {
     			await this.clearCaches(opts);
    @@ -320,6 +341,8 @@ export class UsersService extends ItemsService {
     		await this.knex('directus_versions').update({ user_updated: null }).whereIn('user_updated', keys);
     
     		await super.deleteMany(keys, opts);
    +		await this.clearUserSessions(keys);
    +
     		return keys;
     	}
     
    
  • api/src/utils/get-accountability-for-token.ts+1 0 modified
    @@ -26,6 +26,7 @@ export async function getAccountabilityForToken(
     
     			if ('session' in payload) {
     				await verifySessionJWT(payload);
    +				accountability.session = payload.session;
     			}
     
     			if (payload.share) accountability.share = payload.share;
    
  • packages/types/src/accountability.ts+1 0 modified
    @@ -13,4 +13,5 @@ export type Accountability = {
     	ip: string | null;
     	userAgent?: string;
     	origin?: string;
    +	session?: string;
     };
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.