Files
ayurishchevandClaude Opus 5.5 c4079724c5 Безопасность: проверка секретов, блокировка перебора входа, безопасные редиректы
Безопасность, пункты 1–4 ревью (docs/changes/021):
- приложение не стартует со слабыми секретами: API_TOKEN и SESSION_SECRET
  пустые, change-me или короче 32 символов, ADMIN_PASSWORD короче 12,
  невалидный SECRET_KEY; значения секретов в ошибку не попадают;
- вход в UI: 5 неверных попыток за 10 минут блокируют IP на 10 минут
  (429, пароль не проверяется); неверный пароль — 401; событие
  auth.locked пишется один раз, журнал не засыпается перебором;
- SESSION_COOKIE_SECURE — флаг Secure у cookie сессии (по умолчанию выкл.);
- _safe_next: редирект только на локальный путь (/ui/move, удаление бэкапов);
- .env.bak-* в .gitignore.

Тесты: 28 из 28. Стенд: change-me → 401, новый токен → 200, отказ старта
со слабым токеном, блокировка входа и редиректы проверены, боевые данные
не изменены. Ручная проверка UI пользователем на момент коммита не
подтверждена.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:41:58 +03:00

4.8 KiB
Raw Permalink Blame History

Итоги: 021 — безопасность (по ревью 2026-09-27)

Источник — ревью docs/reviews/2026-09-27-codebase-review.md, раздел «Безопасность» (пункты 1–4).

Сделано

  • п. 1 — проверка секретов при старте (app/config.py::insecure_settings, app/main.py::lifespan — первым шагом): API_TOKEN, SESSION_SECRET — не пустые, не change-me, ≥ 32 символов; ADMIN_PASSWORD — ≥ 12; SECRET_KEY — валидный ключ Fernet. Иначе RuntimeError со списком проблем без значений секретов, приложение не стартует. .env.example — пустые секреты с требованиями и командой генерации.
  • п. 2 — ограничение попыток входа по IP (app/ui/routes.py::login): 5 неверных за 10 минут → IP заблокирован на 10 минут, пароль не проверяется, 429. Неверный пароль — 401 (было 200). Счётчики security.lockout_remaining/register_failure/reset_failures (параметр user → key), ключ login:<ip> не пересекается с ключом очистки журнала. События: auth.failed (IP, оставшиеся попытки), auth.locked — один раз при начале блокировки, auth.login с IP.
  • п. 3 — Secure-cookie: SESSION_COOKIE_SECURE (по умолчанию false), README — включить за TLS.
  • п. 4 — безопасный next (_safe_next): только локальный путь с нужным префиксом; //, \, управляющие символы, схема или хост → адрес по умолчанию. Применён в /ui/move и групповом удалении бэкапов.
  • .env стенда: сгенерированы новые API_TOKEN (43), SESSION_SECRET (64), ADMIN_PASSWORD (20); прежний — .env.bak-20260928; .gitignore — шаблон .env.bak-*. README: «Безопасность», «Настройки», «Ограничения», история изменений.

Найдено на ревью и исправлено

  • Каждая попытка входа с заблокированного IP писала auth.locked — перебором можно было засыпать журнал и вытеснить историю ротацией. Теперь событие пишется один раз.
  • Тест блокировки проверял 303 без follow_redirects=False и падал.
  • Найдено до реализации: на стенде API_TOKEN был change-me (API открыт с общеизвестным токеном), SESSION_SECRET — 10 символов, ADMIN_PASSWORD — 8.

Проверено

  • pytest: 28 из 28 (новые: слабые секреты без утечки значений, отказ старта, блокировка входа по IP, открытый редирект).
  • Отказ старта: временный контейнер без тома с API_TOKEN=change-me → RuntimeError; значений секретов в логе нет.
  • API: change-me → 401, новый токен → 200.
  • Вход: 5 неверных → 401 ×5; 6-я с верным паролем и ещё 3 → 429; с 127.0.0.1 (изнутри контейнера) → 303; события: auth.failed ×4, auth.locked ×1.
  • Редирект: /\evil.com, //evil.com, https://evil.com → /; /?f_group=none сохраняется.
  • Боевые данные: группы 4/4, устройства 14/14, бэкапы 47/47, задачи 73/73 — совпадают по ID; добавлено 7 событий входа проверки. Блокировка снята перезапуском контейнера.

Оговорки

  • Ломающее для эксплуатации: новые пароль администратора и токен API (в .env); старые сессии UI и скрипты со старым токеном не работают.
  • Блокировка входа — в памяти процесса (сбрасывается перезапуском). За reverse-proxy все клиенты видны с IP прокси — нужна настройка доверенных прокси.
  • API-токен не ограничен по попыткам (≥ 32 случайных символа — перебор непрактичен).
  • Ручная проверка UI пользователем на момент коммита не подтверждена.