From 0f8a0da2228bfe826dbfe1a40f1a34e9d74429d2 Mon Sep 17 00:00:00 2001 From: Alpamys Date: Tue, 7 Apr 2026 19:56:51 +0500 Subject: [PATCH] fix(security): apply escapeHtml to all server-supplied innerHTML injections Wrap all server-returned values (run IDs, model names, experiment names, task names, device names, error messages, PIDs, eval results, template names, recipe names) with escapeHtml() before interpolating into innerHTML template literals. Prevents stored XSS via malicious model/experiment names. --- soup_cli/ui/static/app.js | 58 +++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/soup_cli/ui/static/app.js b/soup_cli/ui/static/app.js index 11a289a..9bb8d1c 100644 --- a/soup_cli/ui/static/app.js +++ b/soup_cli/ui/static/app.js @@ -55,7 +55,7 @@ function statusBadge(status) { failed: 'badge-danger', running: 'badge-warning', }; - return `${status}`; + return `${escapeHtml(status)}`; } function truncate(str, len = 30) { @@ -75,7 +75,7 @@ async function loadDashboard() { renderDashboard(); } catch (err) { document.getElementById('dashboard-content').innerHTML = - `
Error loading dashboard: ${err.message}
`; + `
Error loading dashboard: ${escapeHtml(err.message)}
`; } } @@ -110,10 +110,10 @@ function renderDashboard() {
System
- Device: ${systemInfo.device_name}  |  - GPU Memory: ${systemInfo.gpu_info.memory_total}  |  - Python: ${systemInfo.python_version}  |  - Soup: v${systemInfo.version} + Device: ${escapeHtml(systemInfo.device_name)}  |  + GPU Memory: ${escapeHtml(systemInfo.gpu_info.memory_total)}  |  + Python: ${escapeHtml(systemInfo.python_version)}  |  + Soup: v${escapeHtml(systemInfo.version)}
@@ -146,17 +146,17 @@ function renderRunsTable(runs) { ${runs.map(r => ` - - ${r.run_id.substring(0, 20)}... - ${r.experiment_name || '-'} - ${truncate(r.base_model)} - ${r.task || 'sft'} + + ${escapeHtml(r.run_id.substring(0, 20))}... + ${escapeHtml(r.experiment_name || '-')} + ${escapeHtml(truncate(r.base_model))} + ${escapeHtml(r.task || 'sft')} ${statusBadge(r.status)} ${r.final_loss ? r.final_loss.toFixed(4) : '-'} ${formatDuration(r.duration_secs)} ${formatDate(r.created_at)} - + `).join('')} @@ -200,7 +200,7 @@ async function showRunDetail(runId) {
Run ID
-
${run.run_id}
+
${escapeHtml(run.run_id)}
Status
@@ -208,15 +208,15 @@ async function showRunDetail(runId) {
Model
-
${run.base_model || '-'}
+
${escapeHtml(run.base_model || '-')}
Task
-
${run.task || 'sft'}
+
${escapeHtml(run.task || 'sft')}
Device
-
${run.device_name || run.device || '-'}
+
${escapeHtml(run.device_name || run.device || '-')}
Duration
@@ -268,9 +268,9 @@ async function showRunDetail(runId) { ${evalResp.eval_results.map(er => ` - ${er.benchmark} - ${typeof er.score === 'number' ? er.score.toFixed(4) : er.score} - ${er.details_json ? String(er.details_json).substring(0, 100) : '-'} + ${escapeHtml(er.benchmark)} + ${typeof er.score === 'number' ? er.score.toFixed(4) : escapeHtml(String(er.score))} + ${er.details_json ? escapeHtml(String(er.details_json).substring(0, 100)) : '-'} `).join('')} @@ -289,7 +289,7 @@ async function showRunDetail(runId) { renderCharts(metrics); } } catch (err) { - body.innerHTML = `
Error: ${err.message}
`; + body.innerHTML = `
Error: ${escapeHtml(err.message)}
`; } } @@ -389,7 +389,7 @@ async function loadTrainingPage() { renderTrainingPage(templatesResp.templates, statusResp); } catch (err) { document.getElementById('training-content').innerHTML = - `
Error: ${err.message}
`; + `
Error: ${escapeHtml(err.message)}
`; } } @@ -407,14 +407,14 @@ function renderTrainingPage(templates, status) {
@@ -437,7 +437,7 @@ function renderTrainingPage(templates, status) {
Training Status
${status.running - ? `
Running PID: ${status.pid}
+ ? `
Running PID: ${escapeHtml(String(status.pid))}
` : '
No training in progress
' } @@ -491,10 +491,10 @@ async function validateConfig() { if (result.valid) { statusEl.innerHTML = 'Config is valid!'; } else { - statusEl.innerHTML = `Invalid: ${result.error}`; + statusEl.innerHTML = `Invalid: ${escapeHtml(result.error)}`; } } catch (err) { - statusEl.innerHTML = `Error: ${err.message}`; + statusEl.innerHTML = `Error: ${escapeHtml(err.message)}`; } } @@ -512,12 +512,12 @@ async function startTraining() { body: JSON.stringify({ config_yaml: yaml }), }); document.getElementById('config-status').innerHTML = - `Training started! PID: ${result.pid}`; + `Training started! PID: ${escapeHtml(String(result.pid))}`; // Refresh status loadTrainingPage(); } catch (err) { document.getElementById('config-status').innerHTML = - `Error: ${err.message}`; + `Error: ${escapeHtml(err.message)}`; } } @@ -547,7 +547,7 @@ async function inspectData() { }); renderDataResults(result); } catch (err) { - content.innerHTML = `
Error: ${err.message}
`; + content.innerHTML = `
Error: ${escapeHtml(err.message)}
`; } }