VYPR
Moderate severityNVD Advisory· Published Nov 13, 2025· Updated Nov 13, 2025

Directus Vulnerable to Stored Cross-site Scripting

CVE-2025-64747

Description

Directus is a real-time API and App dashboard for managing SQL database content. A stored cross-site scripting (XSS) vulnerability exists in versions prior to 11.13.0 that allows users with upload files and edit item permissions to inject malicious JavaScript through the Block Editor interface. Attackers can bypass Content Security Policy (CSP) restrictions by combining file uploads with iframe srcdoc attributes, resulting in persistent XSS execution. Version 11.13.0 fixes the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
directusnpm
< 11.13.011.13.0

Affected products

1

Patches

1
d23525317f07

Merge from fork (#26108)

https://github.com/directus/directusBrainslugNov 4, 2025via ghsa
4 files changed · +142 11
  • app/src/interfaces/input-block-editor/input-block-editor.vue+2 11 modified
    @@ -3,11 +3,12 @@ import api from '@/api';
     import { useCollectionsStore } from '@/stores/collections';
     import { unexpectedError } from '@/utils/unexpected-error';
     import EditorJS from '@editorjs/editorjs';
    -import { cloneDeep, isEqual } from 'lodash';
    +import { isEqual } from 'lodash';
     import { onMounted, onUnmounted, ref, watch } from 'vue';
     import { useI18n } from 'vue-i18n';
     import { useRouter } from 'vue-router';
     import { useBus } from './bus';
    +import { sanitizeValue } from './sanitize';
     import getTools from './tools';
     import { useFileHandler } from './use-file-handler';
     
    @@ -153,16 +154,6 @@ async function emitValue(context: EditorJS.API | EditorJS) {
     		unexpectedError(error);
     	}
     }
    -
    -function sanitizeValue(value: any): EditorJS.OutputData | null {
    -	if (!value || typeof value !== 'object' || !value.blocks || value.blocks.length < 1) return null;
    -
    -	return cloneDeep({
    -		time: value?.time || Date.now(),
    -		version: value?.version || '0.0.0',
    -		blocks: value.blocks,
    -	});
    -}
     </script>
     
     <template>
    
  • app/src/interfaces/input-block-editor/sanitize.test.ts+88 0 added
    @@ -0,0 +1,88 @@
    +import { describe, expect, test } from 'vitest';
    +import { sanitizeBlockData, sanitizeValue } from './sanitize';
    +
    +describe('sanitizeValue', () => {
    +	describe('input handling', () => {
    +		test('should return null for null input', () => {
    +			expect(sanitizeValue(null)).toBeNull();
    +		});
    +
    +		test('should return null for undefined input', () => {
    +			expect(sanitizeValue(undefined)).toBeNull();
    +		});
    +
    +		test('should return null for non-object input', () => {
    +			expect(sanitizeValue('string')).toBeNull();
    +			expect(sanitizeValue(123)).toBeNull();
    +			expect(sanitizeValue(true)).toBeNull();
    +		});
    +
    +		test('should return null for object without blocks property', () => {
    +			expect(sanitizeValue({ time: 123456 })).toBeNull();
    +		});
    +
    +		test('should return null for object with empty blocks array', () => {
    +			expect(sanitizeValue({ blocks: [] })).toBeNull();
    +		});
    +	});
    +});
    +
    +describe('sanitizeBlockData', () => {
    +	describe('input handling', () => {
    +		test('should return null for null input', () => {
    +			expect(sanitizeBlockData(null)).toBeNull();
    +		});
    +
    +		test('should return null for undefined input', () => {
    +			expect(sanitizeBlockData(undefined)).toBeUndefined();
    +		});
    +	});
    +
    +	describe(() => {
    +		test('should sanitize string data in blocks', () => {
    +			const result = sanitizeBlockData({
    +				type: 'paragraph',
    +				data: '<script>alert("xss")</script>Hello',
    +			});
    +
    +			expect(result.data).toBe('Hello');
    +		});
    +
    +		test('should sanitize object data in blocks', () => {
    +			const result = sanitizeBlockData({
    +				type: 'paragraph',
    +				data: {
    +					text: '<script>alert("xss")</script>Hello',
    +				},
    +			});
    +
    +			expect(result.data.text).toBe('Hello');
    +		});
    +
    +		test('should sanitize nested object data in blocks', () => {
    +			const result = sanitizeBlockData({
    +				type: 'custom',
    +				data: {
    +					level1: {
    +						level2: {
    +							text: '<script>xss</script>Safe text',
    +						},
    +					},
    +				},
    +			});
    +
    +			expect(result.data.level1.level2.text).toBe('Safe text');
    +		});
    +
    +		test('should sanitize array data in blocks', () => {
    +			const result = sanitizeBlockData({
    +				type: 'list',
    +				data: {
    +					items: ['<script>xss</script>Item 1', 'Item 2', 'Item 3'],
    +				},
    +			});
    +
    +			expect(result.data.items[0]).toBe('Item 1');
    +		});
    +	});
    +});
    
  • app/src/interfaces/input-block-editor/sanitize.ts+47 0 added
    @@ -0,0 +1,47 @@
    +import { isObject } from '@directus/utils';
    +import { OutputBlockData } from '@editorjs/editorjs';
    +import dompurify from 'dompurify';
    +import { cloneDeep, isString } from 'lodash';
    +
    +export function sanitizeValue(value: any): EditorJS.OutputData | null {
    +	if (!value || typeof value !== 'object' || !value.blocks || value.blocks.length === 0) return null;
    +
    +	const sanitizedBlocks = value.blocks.map((block: OutputBlockData) => ({
    +		...block,
    +		data: sanitizeBlockData(block.data),
    +	}));
    +
    +	if (sanitizedBlocks.length === 0) return null;
    +
    +	return cloneDeep({
    +		time: value?.time || Date.now(),
    +		version: value?.version || '0.0.0',
    +		blocks: sanitizedBlocks,
    +	});
    +}
    +
    +export function sanitizeBlockData(data: unknown): unknown {
    +	if (Array.isArray(data)) {
    +		return data.map((item: unknown) => sanitizeBlockData(item));
    +	}
    +
    +	if (isObject(data)) {
    +		const cleaned: Record<string, unknown> = {};
    +
    +		for (const key in data) {
    +			if (!Object.prototype.hasOwnProperty.call(data, key)) {
    +				continue;
    +			}
    +
    +			cleaned[key] = sanitizeBlockData(data[key]);
    +		}
    +
    +		return cleaned;
    +	}
    +
    +	if (isString(data)) {
    +		return dompurify.sanitize(data);
    +	}
    +
    +	return data;
    +}
    
  • .changeset/silly-news-prove.md+5 0 added
    @@ -0,0 +1,5 @@
    +---
    +'@directus/app': patch
    +---
    +
    +Improved block editor sanitization
    

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.