# Аудит безопасности и антиспама — WhatIDo > Приложение предназначено для публичного развёртывания. Ниже — результаты аудита > по уровню важности: 🔴 критично (исправить обязательно), 🟠 высокий приоритет, > 🟡 средний приоритет, 🔵 замечания по антиспаму. --- ## 🔴 КРИТИЧНО (исправить обязательно перед публикацией) ### 1. Админ-токен в открытом виде + уязвимость по времени **Файл:** `server.js:52-56` ```js function requireAdmin(req, res, next) { const token = req.headers['x-admin-token']; if (token !== ADMIN_PASSWORD) return res.status(401).json({ error: 'Unauthorized' }); next(); } ``` - Токен сравнивается через `===` — **уязвим к атакам по времени** (timing attack). - Нет хеширования (bcrypt/argon2) — при утечке `.env` или логов токен сразу компрометирован. - Реальный пароль лежит в `.env` на диске. **Рекомендация:** использовать `crypto.timingSafeEqual` и хранить bcrypt-хеш. --- ### 2. Файлы доступны анонимно по токену **Файлы:** `server.js:877-885`, `server.js:49` - `GET /api/files/:token` и статика `/uploads/*` отдают любой файл любому, кто знает токен. - Токены криптостойкие (32 hex), но **фото детей и учебные проекты не должны быть публично доступны по угадываемому ключу**. - Share-ссылки (`/api/share/:token`) также выдают все файлы записи. **Рекомендация:** отдавать файлы только в контексте действующей share-ссылки (проверка принадлежности entry к ссылке) либо подписанные URL с TTL. --- ### 3. Публичные share-ссылки раскрывают ПИД (персональные данные) **Файл:** `server.js:480-531` - `/api/share/:token` возвращает: имена учеников, фото, описания, файлы проектов. - Нет срока действия ссылки (`expires_at`). - Юридический риск (152-ФЗ, GDPR) — данные детей в открытую. **Рекомендация:** добавить `expires_at` в `share_links`, опцию анонимизации имён, требование пароля к ссылке. --- ## 🟠 ВЫСОКИЙ ПРИОРИТЕТ ### 4. Нет IP-based rate limit на публичный POST **Файл:** `server.js:969` - Только интервал по имени студента (`spam_interval_min`, дефолт 30 мин). - Бот может менять имена — ограничение обходится. - Нет лимита по IP — легко завалить сервер или перебрать `/api/files/:token`. **Рекомендация:** добавить `express-rate-limit` по IP на `POST /api/entries`, `GET /api/share/:token`, `GET /api/files/:token`. --- ### 5. Нет honeypot / CAPTCHA **Файл:** `public/index.html:305` - Форма отправки полностью открыта для ботов. - Скрытое поле-ловушка (honeypot) остановит 90% простых ботов. **Рекомендация:** добавить `` и проверку на сервере. --- ### 6. Restore бэкапа загружает 300 МБ в память **Файл:** `server.js:191-194` ```js const uploadBackup = multer({ storage: multer.memoryStorage(), limits: { fileSize: 300 * 1024 * 1024 }, }); ``` - `memoryStorage()` — под нагрузкой DoS (OOM killer). - Архив не проверяется на содержимое до распаковки. **Рекомендация:** использовать `diskStorage` во временную директорию, лимит 50 МБ. --- ### 7. CSP отключён **Файл:** `server.js:47` ```js app.use(helmet({ contentSecurityPolicy: false })); ``` - Весь фронтенд на inline-скриптах и атрибутных обработчиках — строгий CSP их заблокирует. - Без CSP — риск XSS через инъекции в ошибки/настройки. **Рекомендация:** рефакторинг фронтенда на внешние JS-файлы → включить CSP. --- ### 8. Сравнение токена без timing-safe **Файл:** `server.js:54` - `token !== ADMIN_PASSWORD` — уязвим к timing attack. **Рекомендация:** `crypto.timingSafeEqual(Buffer.from(token), Buffer.from(ADMIN_PASSWORD))`. --- ### 9. Поле `files` принимает любые расширения **Файл:** `server.js:83-93` - `fileFilter` проверяет только `photo` (image MIME). - Поле `files` принимает **что угодно** — `.html`, `.js`, `.svg` (SVG может содержать JS). - Имена генерируются случайно, но при угадывании токена — вредоносный файл отдаётся как есть. **Рекомендация:** добавить тот же блок-лист расширений для `files`, отдавать как `download` (не inline). --- ### 10. Нет лимита на размер JSON-body **Файл:** `server.js:48` ```js app.use(express.json()); ``` - Нет `limit` — можно слать огромные JSON, забивать память. **Рекомендация:** `express.json({ limit: '1mb' })`. --- ## 🟡 СРЕДНИЙ ПРИОРИТЕТ | # | Проблема | Файл/Место | |---|----------|------------| | 11 | Share-ссылки никогда не истекают | `db/init.sql:32-41` — нет `expires_at` | | 12 | Нет аудит-лога админ-действий | — | | 13 | Стектрейсы утекают в non-production | `server.js:1155-1157` | | 14 | Нет HSTS / secure cookies / принудительного HTTPS | `server.js:1169-1180` | | 15 | Имена учеников в URL параметрах share | `public/share.html:105` | | 16 | Отсутствует валидация `spam_interval_min` ≥ 1 | `server.js:1004` — можно поставить 0 и отключить антиспам | --- ## 🔵 АНТИСПАМ — ПРОБЕЛЫ | Мера | Статус | Что нужно | |------|--------|-----------| | IP-based rate limit | ❌ | `rateLimit` по IP на `/api/entries` | | Honeypot поле | ❌ | Скрытый input в форме + проверка сервером | | CAPTCHA / Turnstile | ❌ | Опционально: Cloudflare Turnstile | | Мин. интервал спама | ⚠️ Можно 0 | Валидация: минимум 1 минута | | Квота суммарного объёма на IP/день | ❌ | Добавить (напр. 100 МБ/день) | | Лимит файлов на запрос | ✅ 10 файлов | Оставить | | Суммарный размер на запрос | ✅ 30 МБ | Оставить | --- ## ✅ УЖЕ ИСПРАВЛЕНО (подтверждено в текущем коде) - ❌ Убран фолбэк-пароль `'admin'` — старт невозможен без `ADMIN_PASSWORD` - ❌ CORS полностью удалён - ❌ Порт БД 5432 не опубликован, креды из `.env` - ❌ `escapeHtml` исправлен (`&`) - ❌ `express-rate-limit` на API роутах - ❌ Валидация restore-данных + безопасный `safeUnlink` (path traversal защита) - ❌ `helmet` + security-заголовки (кроме CSP) - ❌ Блок-лист расширений загрузки + лимит 30 МБ/запись - ❌ Параметризованные запросы (нет SQL-инъекций) - ❌ Токены файлов криптостойкие (16 байт hex) - ❌ Файлы не перезаписываются (случайные имена) - ❌ Multer-лимиты на размеры есть --- ## 📋 ПЛАН ДЕЙСТВИЙ (must-do перед публикацией) ### Фаза 1 — Критично (до публичного запуска) 1. **Хеш админ-токена** (bcrypt) + `crypto.timingSafeEqual` для сравнения 2. **Защита файлов** — отдача только в контексте валидной share-ссылки или подписанные URL 3. **IP rate limit** на `POST /api/entries` (10 req / 15 мин на IP) 4. **Honeypot** в публичной форме 5. **Backup restore на диск** (diskStorage, лимит 50 МБ) 6. **Мин. spam_interval_min = 1** (валидация в settings) ### Фаза 2 — Укрепление (высокий приоритет) 7. **CSP** — рефакторинг inline-скриптов → внешние файлы 8. **HSTS** через reverse-proxy (Caddy/nginx) 9. **Блок-лист расширений для `files`** + отдача как download 10. **JSON body limit** (`1mb`) 11. **`expires_at` для share_links** + опция анонимизации 12. **Аудит-лог** админ-действий (логин, CRUD, backup/restore) ### Фаза 3 — Приватность и соответствие 13. **Анонимизация share-ссылок** (опция скрыть имена) 14. **Политика хранения** — автоудаление старых записей 15. **Privacy notice** на публичной форме --- ## БЫСТРЫЕ ПОБЕДЫ (можно внедрить сегодня) ```javascript // 1. Timing-safe админ-проверка (server.js:52-56) const crypto = require('crypto'); function requireAdmin(req, res, next) { const token = req.headers['x-admin-token']; const expected = Buffer.from(ADMIN_PASSWORD); const provided = Buffer.from(token || ''); if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) { return res.status(401).json({ error: 'Unauthorized' }); } next(); } // 2. IP rate limit на публичную запись (server.js:969) const entryIpLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10, keyGenerator: req => req.ip, message: { error: 'Too many submissions from this IP' } }); app.post('/api/entries', entryIpLimiter, entryLimiter, ...); // 3. Honeypot в форме (index.html) — скрытое поле // // В хендлере: if (req.body.website) return res.status(400).json({ error: 'Spam detected' }); // 4. JSON body limit (server.js:48) app.use(express.json({ limit: '1mb' })); // 5. Блок-лист для project files (server.js:83-93) const BLOCKED_EXT = /\.(?:html?|js|mjs|cjs|svg|xml|json|map|wasm|php\d?|phtml|asp|aspx|jsp|sh|bat|cmd|cgi|exe|dll|com|msi|scr|hta|vbs|py|r|rb|htaccess)$/i; if (file.fieldname === 'files' && ext && BLOCKED_EXT.test(ext)) return cb(new Error('Not allowed extension')); ``` --- ## Файлы для доработки (приоритет) | Файл | Что править | |------|-------------| | `server.js:52-56` | timing-safe compare, bcrypt-хеш | | `server.js:969` | IP rate limiter + honeypot проверка | | `server.js:191-194` | diskStorage для backup restore | | `server.js:83-93` | блок-лист для `files` | | `server.js:48` | `express.json({ limit: '1mb' })` | | `server.js:1004` | валидация `spam_interval_min >= 1` | | `public/index.html` | honeypot input | | `db/init.sql` | добавить `expires_at` в `share_links` | | `docker-compose.yml` | раскомментировать Caddy для TLS | --- *Аудит выполнен: 2026-09-07* *Статус: готово к внедрению Фазы 1*