Безопасность, пункты 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>
4.8 KiB
4.8 KiB
Итоги: 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 пользователем на момент коммита не подтверждена.