Files
ayurishchevandClaude Opus 5.5 13e17fbb47 Задачи 011-024: доработки по ревью кодовой базы и исправление находок
Ревью кодовой базы (docs/reviews/2026-09-26-codebase-review.md) и планы по каждой находке:
011 IP уникален в VRF и хранится в самом узком префиксе (addresses.vrf_id, составной FK
    с каскадом при переносе VRF, миграция 0007 с остановкой на дублях).
012 Ограничение попыток входа (login_attempts, 429 + Retry-After), выравнивание времени
    ответа, журнал без вытеснения анонимными событиями (миграция 0006).
013 Границы пагинации: отрицательные/чрезмерные limit/offset дают 422 вместо 500.
014 Экран адресов: страница свободных адресов арифметикой, пагинация в SQL.
015 Запрет адреса сети/broadcast, загрузка не выше 100 %.
016 Роль по умолчанию — viewer.
017 Проверка JWT_SECRET/ADMIN_PASSWORD при старте.
018 null в PATCH очищает текстовые поля; нейтральный текст конфликта БД.
019 Пакетная загрузка в списках вместо N+1.
020 Автоназначение адреса вне вложенных префиксов, с блокировкой префикса.
021 Advisory-lock при снятии прав администратора, уникальный lower(username) (миграция 0008).
022 Контейнер не от root, healthcheck, блокировка миграций, requirements.lock.
023 Экранирование LIKE, журнал отказов очистки, заголовки безопасности, учёт force-удаления,
    отзыв токенов при смене пароля (claim pv, миграция 0005).
024 Исправление находок ревью 011-023 (docs/reviews/2026-09-26-changes-011-023-review.md):
    сериализация попыток входа, запрет переноса адресов в адрес сети/broadcast, журнал входов,
    запрет смены своего пароля через PATCH, валидация PATCH устройства, обновлён тест токенов.

Тесты: 14 passed. Документация: README.md, docs/changes/011-024, docs/reviews.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:33:50 +03:00

4.2 KiB

Ограничение попыток входа и защита журнала от вытеснения (изменение 012)

Находка ревью № 2, серьёзность — высокая.

Context

POST /auth/login не ограничен: возможен перебор паролей без авторизации. Каждая неудача пишет session.failed в журнал, а ротация по количеству (max_entries, 100 000) удаляет самые старые записи — анонимный клиент может вытеснить историю изменений. Дополнительно: при несуществующем логине argon2 не вызывается (разница во времени ответа раскрывает существование логина), у пароля нет ограничения длины.

Решение

  1. Учёт попыток — таблица login_attempts (id, ts, client_ip INET, username) (новая миграция), по образцу ClearAttempt; индексы по (client_ip, ts) и (lower(username), ts).
  2. Лимиты (константы в app/api/v1/auth.py, при необходимости — в Settings):
    • по логину: 5 неудач за 10 минут → блокировка входа для этого логина на 10 минут;
    • по IP: 20 неудач за 10 минут → блокировка IP на 10 минут. Ответ при блокировке — 429 {"message", "retry_after_seconds"} + заголовок Retry-After; верный пароль во время блокировки тоже отклоняется. Успешный вход очищает счётчик логина.
  3. Время ответа: для несуществующего логина — проверка по фиктивному argon2-хэшу (вычисляется один раз при старте). LoginIn: username ≤ 100, password ≤ 128 символов.
  4. Журнал без вытеснения:
    • session.failed пишется только для первой неудачи логина/IP в окне; при срабатывании блокировки — одна запись session.locked (diff: логин, IP, число попыток, до какого времени);
    • rotate() при превышении max_entries сначала удаляет самые старые session.failed, затем остальное — события изменений данных уходят последними.
  5. Очистка login_attempts старше суток — в том же часовом цикле ротации.
  6. UI: на экране входа — текст 429 с временем до разблокировки (уже выводится err.message).

Файлы

alembic/versions/<next>_login_attempts.py, app/models.py, app/api/v1/auth.py, app/schemas.py, app/rotation.py, web/app.js (при необходимости), tests/test_journal.py или новый tests/test_auth.py, README.md.

Тест (один сценарий)

6 неверных паролей для временного пользователя → на 6-й 429 с retry_after_seconds; верный пароль тоже 429; в журнале одна session.failed и одна session.locked. Очистка блокировки в тесте — прямым SQL (фикстура db).

Проверка

pytest -q; вручную: серия неверных входов в UI → сообщение о блокировке; через 10 минут вход работает.

Риски

Блокировка по логину позволяет «запереть» чужую учётную запись перебором — поэтому окно короткое (10 минут) и событие видно в журнале. Если стенд за reverse-proxy, без TRUSTED_PROXIES все клиенты делят один IP — лимит по IP заденет всех; отметить в README.