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.
| Package | Affected versions | Patched versions |
|---|---|---|
directusnpm | < 11.13.0 | 11.13.0 |
Affected products
1Patches
1d23525317f07Merge from fork (#26108)
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- github.com/advisories/GHSA-vv2v-pw69-8crfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-64747ghsaADVISORY
- github.com/directus/directus/commit/d23525317f0780f04aa1fe7a99171a358e43cb2eghsax_refsource_MISCWEB
- github.com/directus/directus/security/advisories/GHSA-vv2v-pw69-8crfghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.