Directus missing permission checks for manual trigger Flows
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.
| Package | Affected versions | Patched versions |
|---|---|---|
directusnpm | < 11.9.0 | 11.9.0 |
Affected products
1Patches
122be460c7695Fix manual flows to only trigger with appropriate permissions (#25354)
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- github.com/advisories/GHSA-7cvf-pxgp-42fcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-53889ghsaADVISORY
- github.com/directus/directus/commit/22be460c76957708d67fdd52846a9ad1cbb083fbghsax_refsource_MISCWEB
- github.com/directus/directus/releases/tag/v11.9.0ghsax_refsource_MISCWEB
- github.com/directus/directus/security/advisories/GHSA-7cvf-pxgp-42fcghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.