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:
Maxim Dolgolyov
2026-06-25 09:03:06 +03:00
parent e38abff02a
commit 91917f952c
10 changed files with 74 additions and 9 deletions
+12
View File
@@ -61,6 +61,18 @@ function getOne(req, res) {
`).get(req.params.id);
if (!t) return res.status(404).json({ error: 'Not found' });
// Доступ как в list(): ученик видит только помеченные доступными и не служебные
// экзамен-варианты; учитель — только свои; админ — все. Иначе по id можно было бы
// прочитать тексты заданий из черновиков/вариантов.
const { role, id: uid } = req.user;
const isStudent = role === 'student' || role === 'free_student';
if (isStudent) {
const isVariant = db.prepare('SELECT 1 FROM exam9_variant_tests WHERE test_id = ?').get(t.id);
if (!t.available_to_students || isVariant) return res.status(404).json({ error: 'Not found' });
} else if (role !== 'admin' && t.created_by !== uid) {
return res.status(404).json({ error: 'Not found' });
}
const questions = db.prepare(`
SELECT q.id, q.text, q.type, q.difficulty, q.explanation,
tp.name AS topic, s.name AS subject_name,