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:
@@ -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>`;
|
||||
|
||||
Reference in New Issue
Block a user