Directus allows redacted data extraction on the API through "alias"
Description
Directus is a real-time API and App dashboard for managing SQL database content. A user with permission to view any collection using redacted hashed fields can get access the raw stored version using the alias functionality on the API. Normally, these redacted fields will return ********** however if we change the request to ?alias[workaround]=redacted we can instead retrieve the plain text value for the field. This can be avoided by removing permission to view the sensitive fields entirely from users or roles that should not be able to see them. This vulnerability is fixed in 10.11.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
directusnpm | < 10.11.0 | 10.11.0 |
Affected products
1Patches
1e70a90c267beImproved values redacting (#22332)
4 files changed · +124 −17
api/src/database/run-ast.ts+1 −1 modified@@ -84,7 +84,7 @@ export default async function runAST( // Run the items through the special transforms const payloadService = new PayloadService(collection, { knex, schema }); - let items: null | Item | Item[] = await payloadService.processValues('read', rawItems); + let items: null | Item | Item[] = await payloadService.processValues('read', rawItems, query.alias ?? {}); if (!items || (Array.isArray(items) && items.length === 0)) return items;
api/src/services/payload.test.ts+89 −10 modified@@ -2,7 +2,7 @@ import type { Knex } from 'knex'; import knex from 'knex'; import { MockClient, Tracker, createTracker } from 'knex-mock-client'; import type { MockedFunction } from 'vitest'; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; import type { Helpers } from '../database/helpers/index.js'; import { getHelpers } from '../database/helpers/index.js'; import { PayloadService } from './index.js'; @@ -39,7 +39,7 @@ describe('Integration Tests', () => { }); describe('csv', () => { - it('Returns undefined for illegal values', async () => { + test('Returns undefined for illegal values', async () => { const result = await service.transformers['cast-csv']!({ value: 123, action: 'read', @@ -52,7 +52,7 @@ describe('Integration Tests', () => { expect(result).toBe(undefined); }); - it('Returns [] for empty strings', async () => { + test('Returns [] for empty strings', async () => { const result = await service.transformers['cast-csv']!({ value: '', action: 'read', @@ -65,7 +65,7 @@ describe('Integration Tests', () => { expect(result).toMatchObject([]); }); - it('Returns array values as is', async () => { + test('Returns array values as is', async () => { const result = await service.transformers['cast-csv']!({ value: ['test', 'directus'], action: 'read', @@ -78,7 +78,7 @@ describe('Integration Tests', () => { expect(result).toEqual(['test', 'directus']); }); - it('Splits the CSV string', async () => { + test('Splits the CSV string', async () => { const result = await service.transformers['cast-csv']!({ value: 'test,directus', action: 'read', @@ -91,7 +91,7 @@ describe('Integration Tests', () => { expect(result).toMatchObject(['test', 'directus']); }); - it('Saves array values as joined string', async () => { + test('Saves array values as joined string', async () => { const result = await service.transformers['cast-csv']!({ value: ['test', 'directus'], action: 'create', @@ -104,7 +104,7 @@ describe('Integration Tests', () => { expect(result).toBe('test,directus'); }); - it('Saves string values as is', async () => { + test('Saves string values as is', async () => { const result = await service.transformers['cast-csv']!({ value: 'test,directus', action: 'create', @@ -190,7 +190,7 @@ describe('Integration Tests', () => { }); describe('processes dates', () => { - it('with zero values', () => { + test('with zero values', () => { const result = service.processDates( [ { @@ -211,7 +211,7 @@ describe('Integration Tests', () => { ]); }); - it('with typical values', () => { + test('with typical values', () => { const result = service.processDates( [ { @@ -232,7 +232,7 @@ describe('Integration Tests', () => { ]); }); - it('with date object values', () => { + test('with date object values', () => { const result = service.processDates( [ { @@ -254,6 +254,85 @@ describe('Integration Tests', () => { }); }); }); + + describe('processValues', () => { + let service: PayloadService; + + const concealedField = 'hidden'; + const stringField = 'string'; + const REDACT_STR = '**********'; + + beforeEach(() => { + service = new PayloadService('test', { + knex: db, + schema: { + collections: { + test: { + collection: 'test', + primary: 'id', + singleton: false, + sortField: null, + note: null, + accountability: null, + fields: { + [concealedField]: { + field: concealedField, + defaultValue: null, + nullable: true, + generated: false, + type: 'hash', + dbType: 'nvarchar', + precision: null, + scale: null, + special: ['hash', 'conceal'], + note: null, + validation: null, + alias: false, + }, + [stringField]: { + field: stringField, + defaultValue: null, + nullable: true, + generated: false, + type: 'string', + dbType: 'nvarchar', + precision: null, + scale: null, + special: [], + note: null, + validation: null, + alias: false, + }, + }, + }, + }, + relations: [], + }, + }); + }); + + test('processing special fields', async () => { + const result = await service.processValues('read', { + string: 'not-redacted', + hidden: 'secret', + }); + + expect(result).toMatchObject({ string: 'not-redacted', hidden: REDACT_STR }); + }); + + test('processing aliassed special fields', async () => { + const result = await service.processValues( + 'read', + { + other_string: 'not-redacted', + other_hidden: 'secret', + }, + { other_string: 'string', other_hidden: 'hidden' }, + ); + + expect(result).toMatchObject({ other_string: 'not-redacted', other_hidden: REDACT_STR }); + }); + }); }); });
api/src/services/payload.ts+29 −6 modified@@ -1,5 +1,13 @@ import { ForbiddenError, InvalidPayloadError } from '@directus/errors'; -import type { Accountability, Alterations, Item, PrimaryKey, Query, SchemaOverview } from '@directus/types'; +import type { + Accountability, + Alterations, + Item, + PrimaryKey, + FieldOverview, + Query, + SchemaOverview, +} from '@directus/types'; import { parseJSON, toArray } from '@directus/utils'; import { format, isValid, parseISO } from 'date-fns'; import { unflatten } from 'flat'; @@ -141,30 +149,45 @@ export class PayloadService { processValues(action: Action, payloads: Partial<Item>[]): Promise<Partial<Item>[]>; processValues(action: Action, payload: Partial<Item>): Promise<Partial<Item>>; + processValues(action: Action, payloads: Partial<Item>[], aliasMap: Record<string, string>): Promise<Partial<Item>[]>; + processValues(action: Action, payload: Partial<Item>, aliasMap: Record<string, string>): Promise<Partial<Item>>; async processValues( action: Action, payload: Partial<Item> | Partial<Item>[], + aliasMap: Record<string, string> = {}, ): Promise<Partial<Item> | Partial<Item>[]> { const processedPayload = toArray(payload); if (processedPayload.length === 0) return []; const fieldsInPayload = Object.keys(processedPayload[0]!); + const fieldEntries = Object.entries(this.schema.collections[this.collection]!.fields); + const aliasEntries = Object.entries(aliasMap); - let specialFieldsInCollection = Object.entries(this.schema.collections[this.collection]!.fields).filter( - ([_name, field]) => field.special && field.special.length > 0, - ); + let specialFields: [string, FieldOverview][] = []; + + for (const [name, field] of fieldEntries) { + if (field.special && field.special.length > 0) { + specialFields.push([name, field]); + + for (const [aliasName, fieldName] of aliasEntries) { + if (fieldName === name) { + specialFields.push([aliasName, { ...field, field: aliasName }]); + } + } + } + } if (action === 'read') { - specialFieldsInCollection = specialFieldsInCollection.filter(([name]) => { + specialFields = specialFields.filter(([name]) => { return fieldsInPayload.includes(name); }); } await Promise.all( processedPayload.map(async (record: any) => { await Promise.all( - specialFieldsInCollection.map(async ([name, field]) => { + specialFields.map(async ([name, field]) => { const newValue = await this.processField(field, record, action, this.accountability); if (newValue !== undefined) record[name] = newValue; }),
.changeset/purple-shirts-care.md+5 −0 added@@ -0,0 +1,5 @@ +--- +"@directus/api": patch +--- + +Improved redacting of sensitive values
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- github.com/advisories/GHSA-p8v3-m643-4xqxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-34708ghsaADVISORY
- github.com/directus/directus/commit/e70a90c267bea695afce6545174c2b77517d617bghsax_refsource_MISCWEB
- github.com/directus/directus/security/advisories/GHSA-p8v3-m643-4xqxghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.