Files
WhatIDo/SECURITY_AUDIT_RU.md
T
dev 8a50b46b7b harden anti-spam and file serving; backup restore to disk; json body limit
- Add honeypot field to public submission form + server-side check
- Serve shared files only in context of a valid share link (/api/share/:shareToken/files/:fileToken)
- Switch backup restore upload to diskStorage (50MB) with temp-dir cleanup
- Limit JSON body to 1mb
- Document fixed audit items
2026-09-08 11:36:34 +03:00

12 KiB
Raw Blame History

Аудит безопасности и антиспама — WhatIDo

Приложение предназначено для публичного развёртывания. Ниже — результаты аудита по уровню важности: 🔴 критично (исправить обязательно), 🟠 высокий приоритет, 🟡 средний приоритет, 🔵 замечания по антиспаму.


🔴 КРИТИЧНО (исправить обязательно перед публикацией)

1. Админ-токен в открытом виде + уязвимость по времени

Файл: server.js:52-56

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. Файлы доступны анонимно по токену

Статус: 🟢 Частично исправлено (share-файлы привязаны к ссылке)

  • ✅ Публичные share-файлы теперь отдаются только через GET /api/share/:shareToken/files/:fileToken, где проверяется принадлежность файла к записям активной ссылки (expiry, пароль, фильтры группы/имени/периода) — server.js:612-647.
  • ⚠️ GET /api/files/:token и статика /uploads/* по-прежнему доступны по токену (нужны для админ-панели). Токены криптостойкие (16 байт hex).

Файлы: server.js:612-647, public/share.html:106-121


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% простых ботов.

Рекомендация: добавить <input name="website" style="display:none" tabindex="-1" autocomplete="off"> и проверку на сервере.


6. Restore бэкапа загружает 300 МБ в память

Файл: server.js:191-194

const uploadBackup = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 300 * 1024 * 1024 },
});
  • memoryStorage() — под нагрузкой DoS (OOM killer).
  • Архив не проверяется на содержимое до распаковки.

Рекомендация: использовать diskStorage во временную директорию, лимит 50 МБ.


7. CSP отключён

Файл: server.js:47

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

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 — Укрепление (высокий приоритет)

  1. CSP — рефакторинг inline-скриптов → внешние файлы
  2. HSTS через reverse-proxy (Caddy/nginx)
  3. Блок-лист расширений для files + отдача как download
  4. JSON body limit (1mb)
  5. expires_at для share_links + опция анонимизации
  6. Аудит-лог админ-действий (логин, CRUD, backup/restore)

Фаза 3 — Приватность и соответствие

  1. Анонимизация share-ссылок (опция скрыть имена)
  2. Политика хранения — автоудаление старых записей
  3. Privacy notice на публичной форме

БЫСТРЫЕ ПОБЕДЫ (можно внедрить сегодня)

// 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) — скрытое поле
// <input type="text" name="website" tabindex="-1" autocomplete="off" style="display:none">
// В хендлере: 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