Files
Learn_System/backend/src/routes/avatar.js
T
Maxim Dolgolyov 91917f952c 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>
2026-06-25 09:03:06 +03:00

48 lines
2.4 KiB
JavaScript

const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
const { authMiddleware, requireRole } = require('../middleware/auth');
const { safeExt } = require('../utils/magic');
const ctrl = require('../controllers/avatarController');
/* ── multer: avatars only, 2 MB ────────────────────────────────────────── */
const AVATARS_DIR = path.join(__dirname, '../../uploads/avatars');
const AVATAR_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
const storage = multer.diskStorage({
destination: AVATARS_DIR,
filename: (_req, file, cb) => {
// Расширение — из проверенного MIME (fileFilter уже сузил до image/*),
// НЕ из client-controlled originalname (иначе .html/.svg → stored-XSS).
const ext = safeExt(file.mimetype, '.png');
const name = crypto.randomBytes(16).toString('hex') + ext;
cb(null, name);
},
});
const upload = multer({
storage,
limits: { fileSize: 2 * 1024 * 1024 }, // 2 MB
fileFilter: (_req, file, cb) => {
cb(null, AVATAR_TYPES.has(file.mimetype));
},
});
/* ── student routes ─────────────────────────────────────────────────────── */
router.post('/request', authMiddleware, upload.single('avatar'), ctrl.requestAvatar);
router.get('/my-status', authMiddleware, ctrl.myStatus);
router.delete('/me', authMiddleware, ctrl.removeAvatar);
/* ── preset avatars (available to all roles, no moderation) ─────────────── */
router.get('/presets', authMiddleware, ctrl.listPresets);
router.post('/preset', authMiddleware, ctrl.setPreset);
/* ── moderator routes (teacher or admin) ────────────────────────────────── */
router.get('/pending', authMiddleware, requireRole('teacher', 'admin'), ctrl.getPending);
router.post('/:id/approve', authMiddleware, requireRole('teacher', 'admin'), ctrl.approveAvatar);
router.post('/:id/reject', authMiddleware, requireRole('teacher', 'admin'), ctrl.rejectAvatar);
module.exports = router;