diff --git a/web_interface/blueprints/pages_v3.py b/web_interface/blueprints/pages_v3.py index 0350dba7..73939f52 100644 --- a/web_interface/blueprints/pages_v3.py +++ b/web_interface/blueprints/pages_v3.py @@ -130,6 +130,9 @@ def serve_plugin_web_ui(plugin_id, filename): if not safe_id or not safe_fn: return 'Invalid path component', 400, {'Content-Type': 'text/plain'} + if not pages_v3.plugin_manager: + return 'Plugin manager not available', 503, {'Content-Type': 'text/plain'} + try: _plugins_base = Path(pages_v3.plugin_manager.plugins_dir).resolve() diff --git a/web_interface/static/v3/js/widgets/array-table.js b/web_interface/static/v3/js/widgets/array-table.js index 085b5e35..33e5980e 100644 --- a/web_interface/static/v3/js/widgets/array-table.js +++ b/web_interface/static/v3/js/widgets/array-table.js @@ -119,17 +119,21 @@ let cur = obj; for (let i = 0; i < parts.length - 1; i++) { if (_FORBIDDEN_KEYS.has(parts[i])) return; // block prototype pollution + // eslint-disable-next-line security/detect-object-injection -- key validated by FORBIDDEN_KEYS if (cur[parts[i]] === undefined || typeof cur[parts[i]] !== 'object') { - cur[parts[i]] = {}; + // Object.create(null) produces a null-prototype object that cannot be + // prototype-polluted via __proto__ assignment. + // eslint-disable-next-line security/detect-object-injection -- key validated above + cur[parts[i]] = Object.create(null); } + // eslint-disable-next-line security/detect-object-injection -- key validated above cur = cur[parts[i]]; } const lastKey = parts[parts.length - 1]; - if (!_FORBIDDEN_KEYS.has(lastKey)) cur[lastKey] = value; - } - - function getNestedValue(obj, path) { - return path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj); + if (!_FORBIDDEN_KEYS.has(lastKey)) { + // eslint-disable-next-line security/detect-object-injection -- key validated above + cur[lastKey] = value; + } } function coerceValue(strVal, typeHint) { @@ -407,8 +411,6 @@ const schema = JSON.parse(advancedCell.dataset.propSchema || '{}'); const tbody = row.closest('tbody'); const fieldId = tbody ? tbody.id.replace('_tbody', '') : ''; - const rowIndex = parseInt(row.dataset.index, 10); - // Close any existing modal const existing = document.getElementById('array-row-editor-modal'); if (existing) existing.remove(); @@ -446,6 +448,7 @@ // Section for nested object const section = document.createElement('div'); section.className = 'border border-gray-200 rounded-lg p-3'; + // eslint-disable-next-line no-unsanitized/property -- content sanitized by escapeHtml() section.innerHTML = `

${escapeHtml(label)}

`; const grid = document.createElement('div'); @@ -462,6 +465,7 @@ const currentVal = hiddenInput ? hiddenInput.value : (subSchema.default !== undefined ? subSchema.default : ''); const fieldDiv = document.createElement('div'); + // eslint-disable-next-line no-unsanitized/property -- content sanitized by escapeHtml() fieldDiv.innerHTML = ``; fieldDiv.appendChild(buildModalInput(nestedPath, subSchema, subType, currentVal)); grid.appendChild(fieldDiv); @@ -475,6 +479,7 @@ const currentVal = hiddenInput ? hiddenInput.value : (propSchema.default !== undefined ? propSchema.default : ''); const fieldDiv = document.createElement('div'); + // eslint-disable-next-line no-unsanitized/property -- content sanitized by escapeHtml() fieldDiv.innerHTML = ``; fieldDiv.appendChild(buildModalInput(propName, propSchema, propType, currentVal)); body.appendChild(fieldDiv); @@ -744,9 +749,9 @@ let displayColumns = []; let fullItemProperties = {}; - try { itemProperties = JSON.parse(button.getAttribute('data-item-properties') || '{}'); } catch(e) {} - try { displayColumns = JSON.parse(button.getAttribute('data-display-columns') || '[]'); } catch(e) {} - try { fullItemProperties = JSON.parse(button.getAttribute('data-full-item-properties') || '{}'); } catch(e) { fullItemProperties = itemProperties; } + try { itemProperties = JSON.parse(button.getAttribute('data-item-properties') || '{}'); } catch(_e) {} + try { displayColumns = JSON.parse(button.getAttribute('data-display-columns') || '[]'); } catch(_e) {} + try { fullItemProperties = JSON.parse(button.getAttribute('data-full-item-properties') || '{}'); } catch(_e) { fullItemProperties = itemProperties; } const tbody = document.getElementById(fieldId + '_tbody'); if (!tbody) return; diff --git a/web_interface/static/v3/js/widgets/file-upload-single.js b/web_interface/static/v3/js/widgets/file-upload-single.js index 5ab42542..668c26d8 100644 --- a/web_interface/static/v3/js/widgets/file-upload-single.js +++ b/web_interface/static/v3/js/widgets/file-upload-single.js @@ -126,6 +126,7 @@ html += ``; html += ''; + // eslint-disable-next-line no-unsanitized/property -- all dynamic values sanitized by escapeHtml() container.innerHTML = html; }, @@ -216,10 +217,14 @@ return; } - // Show uploading status + // Show uploading status — use DOM methods to avoid innerHTML with dynamic data if (statusDiv) { statusDiv.className = 'mt-1 text-xs text-gray-500'; - statusDiv.innerHTML = 'Uploading...'; + statusDiv.textContent = ''; + const spinner = document.createElement('i'); + spinner.className = 'fas fa-spinner fa-spin mr-1'; + statusDiv.appendChild(spinner); + statusDiv.appendChild(document.createTextNode('Uploading…')); } const formData = new FormData(); @@ -247,8 +252,12 @@ if (statusDiv) { statusDiv.className = 'mt-1 text-xs text-green-600'; - statusDiv.innerHTML = 'Uploaded successfully'; - setTimeout(() => { statusDiv.className = 'mt-1 text-xs hidden'; statusDiv.innerHTML = ''; }, 3000); + statusDiv.textContent = ''; + const icon = document.createElement('i'); + icon.className = 'fas fa-check-circle mr-1'; + statusDiv.appendChild(icon); + statusDiv.appendChild(document.createTextNode('Uploaded successfully')); + setTimeout(() => { statusDiv.className = 'mt-1 text-xs hidden'; statusDiv.textContent = ''; }, 3000); } notifyFn('Image uploaded successfully', 'success'); } else { @@ -257,7 +266,11 @@ } catch (error) { if (statusDiv) { statusDiv.className = 'mt-1 text-xs text-red-600'; - statusDiv.innerHTML = `${escapeHtml(error.message)}`; + statusDiv.textContent = ''; + const errIcon = document.createElement('i'); + errIcon.className = 'fas fa-exclamation-circle mr-1'; + statusDiv.appendChild(errIcon); + statusDiv.appendChild(document.createTextNode(error.message || 'Upload failed')); } notifyFn(`Upload error: ${error.message}`, 'error'); } finally { diff --git a/web_interface/static/v3/js/widgets/plugin-file-manager.js b/web_interface/static/v3/js/widgets/plugin-file-manager.js index e757bac6..6200f303 100644 --- a/web_interface/static/v3/js/widgets/plugin-file-manager.js +++ b/web_interface/static/v3/js/widgets/plugin-file-manager.js @@ -261,6 +261,8 @@ grid.addEventListener('click', st._gridClickHandler); grid.addEventListener('change', st._gridChangeHandler); + // All dynamic values in the template literal are sanitized by escHtml(). + // eslint-disable-next-line no-unsanitized/property -- values sanitized by escHtml() grid.innerHTML = st.files.map(f => `
@@ -305,6 +307,7 @@ // Build modal using DOM methods so filename never enters a JS string literal. const modal = document.createElement('div'); modal.className = 'pfm-modal'; + // eslint-disable-next-line no-unsanitized/property -- filename sanitized by escHtml() modal.innerHTML = `
${escHtml(filename)} @@ -344,6 +347,7 @@ } else { // Textarea path: _editData stays null; save() reads from the