fix(security): харднинг загрузки файлов, контроль доступа и XSS
Подхвачено из закрытой параллельной сессии (план project_hardening_2026). Загрузки: magic.js получает safeExt/EXT_FOR_MIME — имя файла на диске берёт расширение из проверенного MIME, а не из client originalname (анти stored-XSS .html/.svg). avatar/flashcard/chat-загрузки дополнительно проверяют magic-байты: содержимое должно соответствовать MIME, иначе файл удаляется и 400. Доступ: fileController.getFolderAccess отдаёт список раздачи только владельцу или админу (была утечка имён/email учеников). testController.getOne гейтит видимость как list() — ученик не прочитает тексты заданий черновиков/вариантов по id. XSS: classes.html escJ() экранирует строку для JS-литерала в inline-onclick (имя ученика с кавычкой больше не ломает обработчик). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1034,6 +1034,10 @@
|
||||
|
||||
function fmtDate(d) { return d ? new Date(d).toLocaleDateString('ru',{day:'numeric',month:'short',year:'numeric'}) : '—'; }
|
||||
function pctCls(p) { return p===null?'':p>=75?'pct-hi':p>=50?'pct-mid':'pct-lo'; }
|
||||
// Безопасная подстановка строки в JS-строковый литерал внутри inline-обработчика
|
||||
// (onclick="f('${escJ(name)}')"). esc() не экранирует ' → нужно ещё \-экранировать
|
||||
// обратный слэш и кавычку, иначе имя ученика с ' даёт XSS.
|
||||
function escJ(s) { return esc(String(s ?? '').replace(/\\/g,'\\\\').replace(/'/g,"\\'")); }
|
||||
function toast(msg) {
|
||||
const el = document.getElementById('toast');
|
||||
el.textContent = msg; el.classList.add('show');
|
||||
@@ -1162,7 +1166,7 @@
|
||||
<td><span class="pct-cell ${pc}">${m.avg_pct!==null?m.avg_pct+'%':'—'}</span></td>
|
||||
<td style="color:var(--text-3);font-size:0.78rem">${fmtDate(m.joined_at)}</td>
|
||||
<td>${prepToggleHtml(m.id)}</td>
|
||||
<td><button class="btn-danger" onclick="kickMember(${m.id},'${esc(m.name)}')">Удалить</button></td>
|
||||
<td><button class="btn-danger" onclick="kickMember(${m.id},'${escJ(m.name)}')">Удалить</button></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -1529,7 +1533,7 @@
|
||||
drop.innerHTML = '<div class="student-opt-empty">Ничего не найдено</div>';
|
||||
} else {
|
||||
drop.innerHTML = matches.map(s => `
|
||||
<div class="student-opt" onmousedown="selectStudent(${s.id},'${esc(s.name)}','${esc(s.email)}')">
|
||||
<div class="student-opt" onmousedown="selectStudent(${s.id},'${escJ(s.name)}','${escJ(s.email)}')">
|
||||
<div class="student-opt-name">${esc(s.name)}</div>
|
||||
<div class="student-opt-email">${esc(s.email)}</div>
|
||||
</div>`).join('');
|
||||
@@ -2010,7 +2014,7 @@
|
||||
const rank = (r, i) => {
|
||||
const medal = ['<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89 17 22l-5-3-5 3 1.523-9.11"/></svg>','<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89 17 22l-5-3-5 3 1.523-9.11"/></svg>','<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89 17 22l-5-3-5 3 1.523-9.11"/></svg>'][i] || (i+1)+'.';
|
||||
const pc = pctCls(r.percent);
|
||||
const clickable = r.session_id ? `onclick="openResDrill(${r.session_id},'${esc(r.name)}',${r.percent??'null'})"` : '';
|
||||
const clickable = r.session_id ? `onclick="openResDrill(${r.session_id},'${escJ(r.name)}',${r.percent??'null'})"` : '';
|
||||
return `<div class="res-row" ${clickable} title="${r.session_id ? 'Посмотреть ответы' : ''}">
|
||||
<div class="res-rank">${medal}</div>
|
||||
<div class="res-name">
|
||||
|
||||
Reference in New Issue
Block a user