From bcc2d3b73988226ae53650fcba3b4c82b91873a5 Mon Sep 17 00:00:00 2001 From: Mark McIntosh Date: Thu, 23 Jul 2026 01:14:28 -0400 Subject: [PATCH] fix(dynamic-field): escape media field VALUE against stored XSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to #1027 (number/date/datetime value): the media value/url is attacker-controlled content data (stored raw by the write path) and rendered back into the content-edit form. - New local escapeJsAttr helper (\xHH) — for a single-quoted JS string inside an HTML attribute, where escapeHtml is reversible (the browser HTML-decodes the attribute before the inline onclick parses). - media case: media value/url escaped in its HTML-attr sinks (video/img src, hidden value, data-url) via escapeHtml; the url in removeMediaFromMultiple(...) via escapeJsAttr. Not in scope (schema-admin defense-in-depth): the field_options attrs + id/name-from-field_name sinks; the root fix there is slug-validating field_name on write. --- .../components/dynamic-field.template.ts | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/core/src/templates/components/dynamic-field.template.ts b/packages/core/src/templates/components/dynamic-field.template.ts index fe6b6c4fc..3ee686bf1 100644 --- a/packages/core/src/templates/components/dynamic-field.template.ts +++ b/packages/core/src/templates/components/dynamic-field.template.ts @@ -891,14 +891,14 @@ export function renderDynamicField(field: FieldDefinition, options: FieldRenderO const renderMediaPreview = (url: unknown, alt: string, classes: string) => { if (typeof url !== 'string' || url === '') return '' if (isVideoUrl(url)) { - return `` + return `` } - return `${alt}` + return `${escapeHtml(alt)}` } fieldHTML = `
- + ${ isMultiple @@ -907,11 +907,11 @@ export function renderDynamicField(field: FieldDefinition, options: FieldRenderO ${mediaValues .map( (url: string, idx: number) => ` -
+
${renderMediaPreview(url, `Media ${idx + 1}`, 'w-full h-24 object-cover rounded-lg border border-white/20')}