@@ -891,14 +891,14 @@ export function renderDynamicField(field: FieldDefinition, options: FieldRenderO
891891 const renderMediaPreview = ( url : unknown , alt : string , classes : string ) => {
892892 if ( typeof url !== 'string' || url === '' ) return ''
893893 if ( isVideoUrl ( url ) ) {
894- return `<video src="${ url } " class="${ classes } " muted></video>`
894+ return `<video src="${ escapeHtml ( url ) } " class="${ classes } " muted></video>`
895895 }
896- return `<img src="${ url } " alt="${ alt } " class="${ classes } ">`
896+ return `<img src="${ escapeHtml ( url ) } " alt="${ escapeHtml ( alt ) } " class="${ classes } ">`
897897 }
898898
899899 fieldHTML = `
900900 <div class="media-field-container">
901- <input type="hidden" id="${ fieldId } " name="${ fieldName } " value="${ isMultiple ? mediaValues . join ( ',' ) : singleValue } " data-multiple="${ isMultiple } ">
901+ <input type="hidden" id="${ fieldId } " name="${ fieldName } " value="${ escapeHtml ( String ( isMultiple ? mediaValues . join ( ',' ) : singleValue ) ) } " data-multiple="${ isMultiple } ">
902902
903903 ${
904904 isMultiple
@@ -907,11 +907,11 @@ export function renderDynamicField(field: FieldDefinition, options: FieldRenderO
907907 ${ mediaValues
908908 . map (
909909 ( url : string , idx : number ) => `
910- <div class="relative media-preview-item" data-url="${ url } ">
910+ <div class="relative media-preview-item" data-url="${ escapeHtml ( url ) } ">
911911 ${ renderMediaPreview ( url , `Media ${ idx + 1 } ` , 'w-full h-24 object-cover rounded-lg border border-white/20' ) }
912912 <button
913913 type="button"
914- onclick="removeMediaFromMultiple('${ fieldId } ', '${ url } ')"
914+ onclick="removeMediaFromMultiple('${ fieldId } ', '${ escapeJsAttr ( url ) } ')"
915915 data-media-remove="true"
916916 class="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 hover:bg-red-700"
917917 ${ disabled ? 'disabled' : '' }
@@ -2648,3 +2648,22 @@ function escapeHtml(text: string): string {
26482648 "'" : '''
26492649 } [ char ] || char ) )
26502650}
2651+
2652+ /**
2653+ * Escape a string for a single-quoted JS string inside an HTML attribute —
2654+ * e.g. onclick="f('${escapeJsAttr(x)}')". escapeHtml is insufficient: the browser
2655+ * HTML-decodes the attribute BEFORE the inline script parses, so ' decodes back
2656+ * to ' and terminates the string. Emits \xHH escapes so no raw '"<>&\ survives.
2657+ */
2658+ function escapeJsAttr ( value : unknown ) : string {
2659+ return String ( value ?? '' ) . replace ( / [ \\ ' " < > & \r \n ] / g, ( ch ) => ( {
2660+ '\\' : '\\\\' ,
2661+ "'" : '\\x27' ,
2662+ '"' : '\\x22' ,
2663+ '<' : '\\x3C' ,
2664+ '>' : '\\x3E' ,
2665+ '&' : '\\x26' ,
2666+ '\r' : '\\r' ,
2667+ '\n' : '\\n'
2668+ } [ ch ] || ch ) )
2669+ }
0 commit comments