VYPR
Moderate severityNVD Advisory· Published May 13, 2024· Updated Aug 2, 2024

Directus allows redacted data extraction on the API through "alias"

CVE-2024-34708

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.

PackageAffected versionsPatched versions
directusnpm
< 10.11.010.11.0

Affected products

1

Patches

1
e70a90c267be

Improved values redacting (#22332)

https://github.com/directus/directusBrainslugMay 2, 2024via ghsa
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

News mentions

0

No linked articles in our index yet.