# Аудит безопасности и антиспама — 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*