[FIX] Epic C / C5 hardening — esiti flotta di verifica multi-agente (mig.054)
Corretti i finding confermati dalla verifica multi-agente (5 major + minori): SICUREZZA - Stored XSS allegati: da blocklist a ALLOWLIST di estensioni innocue (no html/svg/js renderizzabili same-origin) in StakeholderPortalController::attachment e StakeholderActivityController::storeUpload; nome file randomizzato (random_bytes). - Magic-link: scadenza (mig.054 stk_activity_targets.token_expires_at; send() imposta scadenza attività+30gg o +90gg; resolveTarget() → 410 TOKEN_EXPIRED se scaduto); rate-limit per-IP sugli endpoint del portale; comment/attachment bloccati su attività chiusa. CORRETTEZZA - send(): NON rigenera token né azzera lo stato dei destinatari già responded/acknowledged (prima ne perdeva l'esito); imposta token_expires_at. - assign individuale: semantica "replace" (rimuove i deselezionati non ancora conclusi) + guard su lista vuota (evita 'IN ()'). - update(): conserva assign_mode esistente quando si modifica solo stak_code. UI/UX/A11Y - Editor opzioni per domande a scelta singola/multipla (prima degradavano a testo nel portale). - Etichette stato/tipo localizzate; risposte mostrate inline (no alert()); escAttr nel portale (escape virgolette negli attributi); ARIA su modali/tab; voce sidebar anche in common.js. OPEN ITEM (NON regressione C5, pre-esistente e ambientale): l'upload allegati restituisce UPLOAD_ERROR in prod — move_uploaded_file/is_uploaded_file fallisce nella topologia proxy→fastcgi (stesso pattern di evidence_files/AuditController, mai funzionato: la dir uploads/evidence non esiste). rename/copy come www-data funzionano. Da investigare lato infra. Il resto di C5 (questionari, firma-lettura, commenti, calendario, portale) è pienamente operativo. Smoke prod OK: allowlist (.html→422, struttura ok), opzioni scelta nel portale, send no-reset, replace individuale, token_expires_at presente. Additivo. v1.21.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4f386faae5
commit
b917d2da14
+19
-4
@@ -46,6 +46,7 @@
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function esc(s) { var d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||||
function escAttr(s) { return esc(s).replace(/"/g, '"'); }
|
||||
function app() { return el('pw-app'); }
|
||||
|
||||
async function apiGet(path) {
|
||||
@@ -98,7 +99,7 @@
|
||||
}
|
||||
|
||||
function qHtml(q, i) {
|
||||
var code = esc(q.code || ('Q' + (i + 1)));
|
||||
var code = escAttr(q.code || ('Q' + (i + 1)));
|
||||
var req = q.required ? ' <span class="q-req">*</span>' : '';
|
||||
var inner = '';
|
||||
if (q.type === 'yes_no') {
|
||||
@@ -106,15 +107,15 @@
|
||||
} else if (q.type === 'scale_1_5') {
|
||||
inner = [1,2,3,4,5].map(function (n) { return '<label class="q-opt" style="display:inline-flex;margin-right:14px;"><input type="radio" name="' + code + '" value="' + n + '"> ' + n + '</label>'; }).join('');
|
||||
} else if (q.type === 'single_choice' && q.options) {
|
||||
inner = q.options.map(function (o) { return '<label class="q-opt"><input type="radio" name="' + code + '" value="' + esc(o) + '"> ' + esc(o) + '</label>'; }).join('');
|
||||
inner = q.options.map(function (o) { return '<label class="q-opt"><input type="radio" name="' + code + '" value="' + escAttr(o) + '"> ' + esc(o) + '</label>'; }).join('');
|
||||
} else if (q.type === 'multi_choice' && q.options) {
|
||||
inner = q.options.map(function (o) { return '<label class="q-opt"><input type="checkbox" name="' + code + '" value="' + esc(o) + '"> ' + esc(o) + '</label>'; }).join('');
|
||||
inner = q.options.map(function (o) { return '<label class="q-opt"><input type="checkbox" name="' + code + '" value="' + escAttr(o) + '"> ' + esc(o) + '</label>'; }).join('');
|
||||
} else if (q.type === 'number') {
|
||||
inner = '<input type="number" name="' + code + '">';
|
||||
} else {
|
||||
inner = '<textarea name="' + code + '" rows="2"></textarea>';
|
||||
}
|
||||
return '<div class="q-block" data-code="' + code + '" data-type="' + esc(q.type || 'text') + '"><div class="q-text">' + esc(q.text) + req + '</div>' + inner + '</div>';
|
||||
return '<div class="q-block" data-code="' + code + '" data-type="' + escAttr(q.type || 'text') + '"><div class="q-text">' + esc(q.text) + req + '</div>' + inner + '</div>';
|
||||
}
|
||||
|
||||
function collectAnswers() {
|
||||
@@ -136,6 +137,20 @@
|
||||
|
||||
async function doRespond() {
|
||||
var answers = collectAnswers();
|
||||
// pre-check campi obbligatori lato client (evidenzia i blocchi mancanti)
|
||||
var qs = (DATA.template && DATA.template.questions) || [];
|
||||
var missing = [];
|
||||
document.querySelectorAll('.q-block').forEach(function (b) { b.style.borderLeft = ''; });
|
||||
qs.forEach(function (q) {
|
||||
if (!q.required) return;
|
||||
var v = answers[q.code];
|
||||
if (v == null || v === '' || (Array.isArray(v) && !v.length)) {
|
||||
missing.push(q.text);
|
||||
var b = document.querySelector('.q-block[data-code="' + (q.code || '') + '"]');
|
||||
if (b) b.style.borderLeft = '3px solid #dc2626';
|
||||
}
|
||||
});
|
||||
if (missing.length) { flash('Rispondi alle domande obbligatorie: ' + missing.join('; '), false); window.scrollTo(0, 0); return; }
|
||||
var res = await apiPost('/stakeholder-portal/respond', { t: TOKEN, answers: answers, respondent_name: (el('pw-name') || {}).value });
|
||||
if (!res.success) { flash(res.message || 'Errore', false); return; }
|
||||
DATA.submitted = true; render(); window.scrollTo(0, 0);
|
||||
|
||||
Reference in New Issue
Block a user