VYPR
Moderate severityNVD Advisory· Published Jul 14, 2025· Updated Jul 15, 2025

Directus missing permission checks for manual trigger Flows

CVE-2025-53889

Description

Directus is a real-time API and App dashboard for managing SQL database content. Starting in version 9.12.0 and prior to version 11.9.0, Directus Flows with a manual trigger are not validating whether the user triggering the Flow has permissions to the items provided as payload to the Flow. Depending on what the Flow is set up to do this can lead to the Flow executing potential tasks on the attacker's behalf without authenticating. Bad actors could execute the manual trigger Flows without authentication, or access rights to the said collection(s) or item(s). Users with manual trigger Flows configured are impacted as these endpoints do not currently validate if the user has read access to directus_flows or to the relevant collection/items. The manual trigger Flows should have tighter security requirements as compared to webhook Flows where users are expected to perform do their own checks. Version 11.9.0 fixes the issue. As a workaround, implement permission checks for read access to Flows and read access to relevant collection/items.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
directusnpm
< 11.9.011.9.0

Affected products

1

Patches

1
22be460c7695

Fix manual flows to only trigger with appropriate permissions (#25354)

https://github.com/directus/directusBrainslugJun 25, 2025via ghsa
2 files changed · +68 1
  • api/src/flows.ts+63 1 modified
    @@ -12,13 +12,16 @@ import { useBus } from './bus/index.js';
     import getDatabase from './database/index.js';
     import emitter from './emitter.js';
     import { useLogger } from './logger/index.js';
    +import { fetchPermissions } from './permissions/lib/fetch-permissions.js';
    +import { fetchPolicies } from './permissions/lib/fetch-policies.js';
     import { ActivityService } from './services/activity.js';
     import { FlowsService } from './services/flows.js';
     import * as services from './services/index.js';
     import { RevisionsService } from './services/revisions.js';
     import type { EventHandler } from './types/index.js';
     import { constructFlowTree } from './utils/construct-flow-tree.js';
     import { getSchema } from './utils/get-schema.js';
    +import { getService } from './utils/get-service.js';
     import { JobQueue } from './utils/job-queue.js';
     import { redactObject } from './utils/redact-object.js';
     import { scheduleSynchronizedJob, validateCron } from './utils/schedule.js';
    @@ -120,7 +123,7 @@ class FlowManager {
     	public async runWebhookFlow(
     		id: string,
     		data: unknown,
    -		context: Record<string, unknown>,
    +		context: { schema: SchemaOverview; accountability: Accountability | undefined } & Record<string, unknown>,
     	): Promise<{ result: unknown; cacheEnabled?: boolean }> {
     		const logger = useLogger();
     
    @@ -248,7 +251,9 @@ class FlowManager {
     			} else if (flow.trigger === 'manual') {
     				const handler = async (data: unknown, context: Record<string, unknown>) => {
     					const enabledCollections = flow.options?.['collections'] ?? [];
    +					const requireSelection = flow.options?.['requireSelection'] ?? true;
     					const targetCollection = (data as Record<string, any>)?.['body'].collection;
    +					const targetKeys = (data as Record<string, any>)?.['body'].keys;
     
     					if (!targetCollection) {
     						logger.warn(`Manual trigger requires "collection" to be specified in the payload`);
    @@ -265,6 +270,63 @@ class FlowManager {
     						throw new ForbiddenError();
     					}
     
    +					if (!targetKeys || !Array.isArray(targetKeys)) {
    +						logger.warn(`Manual trigger requires "keys" to be specified in the payload`);
    +						throw new ForbiddenError();
    +					}
    +
    +					if (requireSelection && targetKeys.length === 0) {
    +						logger.warn(`Manual trigger requires at least one key to be specified in the payload`);
    +						throw new ForbiddenError();
    +					}
    +
    +					const accountability = context?.['accountability'] as Accountability | undefined;
    +
    +					if (!accountability) {
    +						logger.warn(`Manual flows are only triggerable when authenticated`);
    +						throw new ForbiddenError();
    +					}
    +
    +					if (accountability.admin === false) {
    +						const database = (context['database'] as Knex) ?? getDatabase();
    +						const schema = (context['schema'] as SchemaOverview) ?? (await getSchema({ database }));
    +
    +						const policies = await fetchPolicies(accountability, { schema, knex: database });
    +
    +						const permissions = await fetchPermissions(
    +							{
    +								policies,
    +								accountability,
    +								action: 'read',
    +								collections: [targetCollection],
    +							},
    +							{ schema, knex: database },
    +						);
    +
    +						if (permissions.length === 0) {
    +							logger.warn(`Triggering ${targetCollection} is not allowed`);
    +							throw new ForbiddenError();
    +						}
    +
    +						const service = getService(targetCollection, { schema, accountability, knex: database });
    +						const primaryField = schema.collections[targetCollection]!.primary;
    +
    +						let keys = await service.readMany(
    +							targetKeys,
    +							{ fields: [primaryField] },
    +							{
    +								emitEvents: false,
    +							},
    +						);
    +
    +						keys = keys.map((key) => key[primaryField]);
    +
    +						if (targetKeys.some((key) => !keys.includes(key))) {
    +							logger.warn(`Triggering keys ${targetKeys} is not allowed`);
    +							throw new ForbiddenError();
    +						}
    +					}
    +
     					if (flow.options['async']) {
     						this.executeFlow(flow, data, context);
     						return { result: undefined };
    
  • .changeset/seven-flies-exist.md+5 0 added
    @@ -0,0 +1,5 @@
    +---
    +'@directus/api': major
    +---
    +
    +Fixed manual flows to only trigger with appropriate permissions
    

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

5

News mentions

0

No linked articles in our index yet.