v1.2.19: fixes from Codex audit — XSS in params, JS injection in onclick, scenario polling timeout, has_target validation, tracker atomic update

This commit is contained in:
2026-07-31 17:58:57 +04:00
parent e0e16b91a8
commit 58b823a260
4 changed files with 88 additions and 31 deletions
+28 -5
View File
@@ -116,16 +116,21 @@ async function toggleScenarioSteps(defId) {
if (s.instance_ref) html += ` <span style="color:var(--muted);">→ ${_esc(s.instance_ref)}</span>`;
if (s.instance_uid) html += ` <span style="color:var(--muted);">→ ${_esc(s.instance_uid.substring(0,8))}...</span>`;
const params = Object.entries(s.params || {});
if (params.length) html += ' <span style="color:var(--muted);">(' + params.map(([k,v]) => k+'='+v).join(', ') + ')</span>';
if (params.length) html += ' <span style="color:var(--muted);">(' + params.map(([k,v]) => _esc(k)+'='+_esc(v)).join(', ') + ')</span>';
html += '</div>';
});
// Кнопки действий
// JS-escape для имени сценария в inline onclick:
// \ → \\ (backslash)
// ' → \' (terminate JS string literal)
// HTML-escape уже не нужен — внутри JS-строки в атрибуте HTML-теги не парсятся.
const escName = def.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
html += `<button class="btn btn-sm" style="margin-top:4px;font-size:11px;background:var(--brand-primary);color:#fff;" onclick="event.stopPropagation();runScenario(${defId})">▶ Запустить</button>`;
html += `<button class="btn btn-sm" style="margin-top:4px;margin-left:4px;font-size:11px;" onclick="event.stopPropagation();editScenario(${defId})">✏ Редактировать</button>`;
html += `<button class="btn btn-sm" style="margin-top:4px;margin-left:4px;font-size:11px;" onclick="event.stopPropagation();cloneScenario(${defId},'${_esc(def.name)}')">📋 Копировать</button>`;
html += `<button class="btn btn-sm" style="margin-top:4px;margin-left:4px;font-size:11px;" onclick="event.stopPropagation();cloneScenario(${defId},'${escName}')">📋 Копировать</button>`;
// Удалить — только для НЕ-seed сценариев
if (!def.is_seed) html += `<button class="btn btn-sm" style="margin-top:4px;margin-left:4px;font-size:11px;color:var(--destructive);" onclick="event.stopPropagation();deleteScenario(${defId},'${_esc(def.name)}')">🗑 Удалить</button>`;
if (!def.is_seed) html += `<button class="btn btn-sm" style="margin-top:4px;margin-left:4px;font-size:11px;color:var(--destructive);" onclick="event.stopPropagation();deleteScenario(${defId},'${escName}')">🗑 Удалить</button>`;
html += '</div>';
el.innerHTML = html;
@@ -182,10 +187,21 @@ async function runScenario(defId){
}
// Поллинг статуса
// Поллинг статуса с защитой от бесконечного зависания
let _scenarioPollErrors = 0;
scenarioPollTimer=setInterval(async()=>{
try{
const sr=await fetch('/api/scenario/run/'+runId);
if(!sr.ok){ return; }
if(!sr.ok){
_scenarioPollErrors++;
if(_scenarioPollErrors>=5){
stopScenarioPoll();
busy=false;
body.innerHTML='<span style="color:var(--destructive);">Ошибка: сервер недоступен</span>';
}
return;
}
_scenarioPollErrors = 0; // сброс при успехе
const last=await sr.json();
if(!last||last.error) return;
@@ -203,7 +219,14 @@ async function runScenario(defId){
await refreshInstances(); // обновить список инстансов
if(historyOpen) await loadHistory(); // обновить историю
}
}catch(e){}
}catch(e){
_scenarioPollErrors++;
if(_scenarioPollErrors>=5){
stopScenarioPoll();
busy=false;
body.innerHTML=`<span style="color:var(--destructive);">Ошибка поллинга: ${_esc(e.message||'')}</span>`;
}
}
},3000);
}catch(e){
body.innerHTML=`<span style="color:var(--destructive);">Ошибка: ${_esc(e.message)}</span>`;