CVE-2024-34708 (TypeScript, published 2024-05-14T15:39Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/directus/directus
Files changed:
.changeset/purple-shirts-care.md
api/src/database/run-ast.ts
api/src/services/payload.test.ts
api/src/services/payload.ts
Commit message: Improved values redacting (#22332)

Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>

Vulnerable code:
		let items: null | Item | Item[] = await payloadService.processValues('read', rawItems);
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
				it('Returns undefined for illegal values', async () => {
				it('Returns [] for empty strings', async () => {
				it('Returns array values as is', async () => {
				it('Splits the CSV string', async () => {
				it('Saves array values as joined string', async () => {
				it('Saves string values as is', async () => {
				it('with zero values', () => {
				it('with typical values', () => {
				it('with date object values', () => {
import type { Accountability, Alterations, Item, PrimaryKey, Query, SchemaOverview } from '@directus/types';
		let specialFieldsInCollection = Object.entries(this.schema.collections[this.collection]!.fields).filter(
			([_name, field]) => field.special && field.special.length > 0,
		);
			specialFieldsInCollection = specialFieldsInCollection.filter(([name]) => {
					specialFieldsInCollection.map(async ([name, field]) => {

Fixed code:
---
"@directus/api": patch
---

Improved redacting of sensitive values
		let items: null | Item | Item[] = await payloadService.processValues('read', rawItems, query.alias ?? {});
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
				test('Returns undefined for illegal values', async () => {
				test('Returns [] for empty strings', async () => {
				test('Returns array values as is', async () => {
				test('Splits the CSV string', async () => {
				test('Saves array values as joined string', async () => {
				test('Saves string values as is', async () => {
				test('with zero values', () => {
				test('with typical values', () => {
				test('with date object values', () => {

		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 });
			});
		});
import type {
	Accountability,
	Alterations,
	Item,
	PrimaryKey,
	FieldOverview,
	Query,
	SchemaOverview,
} from '@directus/types';
	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>>;
		aliasMap: Record<string, string> = {},
		const fieldEntries = Object.entries(this.schema.collections[this.collection]!.fields);
		const aliasEntries = Object.entries(aliasMap);
		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 }]);
					}
				}
			}
		}
			specialFields = specialFields.filter(([name]) => {
					specialFields.map(async ([name, field]) => {