fix: Add HTML escaping to prevent XSS in fallback code and checkbox-group

Add proper HTML escaping for schema-derived values to prevent XSS vulnerabilities
in fallback rendering code and checkbox-group widget.

Problem:
- Fallback code in generateFieldHtml (line 3094) doesn't escape propLabel
  when building HTML strings, while main renderArrayObjectItem uses escapeHtml()
- Checkbox-group widget (lines 3012-3025) doesn't escape option or label values
- While risk is limited (values come from plugin schemas), malicious plugin
  schemas or untrusted schema sources could inject XSS
- Inconsistent with main renderArrayObjectItem which properly escapes

Solution:
- Added escapeHtml() calls for propLabel in fallback array-of-objects rendering
  (both locations: generateFieldHtml and addArrayObjectItem fallback)
- Added escapeHtml() calls for option values in checkbox-group widget:
  - checkboxId (contains option)
  - data-option-value attribute
  - value attribute
  - label text in span
- Ensures consistent XSS protection across all rendering paths

This prevents potential XSS if plugin schemas contain malicious HTML/script
content in enum values or property titles.
This commit is contained in:
Chuck
2026-01-08 15:33:04 -05:00
parent 36f948affc
commit 52d835cbad

View File

@@ -3012,18 +3012,18 @@ function generateFieldHtml(key, prop, value, prefix = '') {
enumItems.forEach((option) => { enumItems.forEach((option) => {
const isChecked = arrayValue.includes(option); const isChecked = arrayValue.includes(option);
const label = labels[option] || option.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); const label = labels[option] || option.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const checkboxId = `${fieldId}_${option}`; const checkboxId = `${fieldId}_${escapeHtml(option)}`;
html += ` html += `
<label class="flex items-center"> <label class="flex items-center">
<input type="checkbox" <input type="checkbox"
id="${checkboxId}" id="${checkboxId}"
data-checkbox-group="${fieldId}" data-checkbox-group="${fieldId}"
data-option-value="${option}" data-option-value="${escapeHtml(option)}"
value="${option}" value="${escapeHtml(option)}"
${isChecked ? 'checked' : ''} ${isChecked ? 'checked' : ''}
onchange="updateCheckboxGroupData('${fieldId}')" onchange="updateCheckboxGroupData('${fieldId}')"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"> class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<span class="ml-2 text-sm text-gray-700">${label}</span> <span class="ml-2 text-sm text-gray-700">${escapeHtml(label)}</span>
</label> </label>
`; `;
}); });
@@ -3092,7 +3092,7 @@ function generateFieldHtml(key, prop, value, prefix = '') {
const propSchema = itemProperties[propKey]; const propSchema = itemProperties[propKey];
const propValue = item[propKey] !== undefined ? item[propKey] : propSchema.default; const propValue = item[propKey] !== undefined ? item[propKey] : propSchema.default;
const propLabel = propSchema.title || propKey.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); const propLabel = propSchema.title || propKey.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
html += `<div class="mb-3"><label class="block text-sm font-medium text-gray-700 mb-1">${propLabel}</label>`; html += `<div class="mb-3"><label class="block text-sm font-medium text-gray-700 mb-1">${escapeHtml(propLabel)}</label>`;
if (propSchema.type === 'boolean') { if (propSchema.type === 'boolean') {
const checked = propValue ? 'checked' : ''; const checked = propValue ? 'checked' : '';
html += `<input type="checkbox" data-prop-key="${propKey}" ${checked} class="h-4 w-4 text-blue-600" onchange="window.updateArrayObjectData('${fieldId}')">`; html += `<input type="checkbox" data-prop-key="${propKey}" ${checked} class="h-4 w-4 text-blue-600" onchange="window.updateArrayObjectData('${fieldId}')">`;
@@ -6497,7 +6497,7 @@ if (typeof window !== 'undefined') {
const propSchema = itemsSchema.properties[propKey]; const propSchema = itemsSchema.properties[propKey];
const propValue = newItem[propKey] !== undefined ? newItem[propKey] : propSchema.default; const propValue = newItem[propKey] !== undefined ? newItem[propKey] : propSchema.default;
const propLabel = propSchema.title || propKey.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); const propLabel = propSchema.title || propKey.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
itemHtml += `<div class="mb-3"><label class="block text-sm font-medium text-gray-700 mb-1">${propLabel}</label>`; itemHtml += `<div class="mb-3"><label class="block text-sm font-medium text-gray-700 mb-1">${escapeHtml(propLabel)}</label>`;
if (propSchema.type === 'boolean') { if (propSchema.type === 'boolean') {
const checked = propValue ? 'checked' : ''; const checked = propValue ? 'checked' : '';
// No name attribute - rely solely on _data field to prevent key leakage // No name attribute - rely solely on _data field to prevent key leakage