fix(tradein/auth): доля слотов сверки пароля на адрес — потолок перестаёт бить по своим (#2714) #2717

Merged
bot-backend merged 2 commits from fix/2714-verify-slot-fairness into main 2026-08-06 10:42:39 +00:00

2 commits

Author SHA1 Message Date
57d3fec137 test(tradein/auth): покрыть отказ пула и отмену в очереди + записать цену размена по NAT (#2714)
All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 3m5s
Правки по глубокому ревью.

Тесты:
- отказ пула на `submit` (единственная изменённая строка без покрытия): слот
  отдаётся синхронно, оба счётчика в нуле. Мутант «старый код, ключевой
  счётчик не отдан» краснит и тест, и autouse-сторож.
- отмена ЕЩЁ НЕ НАЧАТОЙ работы возвращает слот ключа — ветка future другая,
  чем у отмены начатой, соседний тест её не покрывал.
- пол `max(1, …)`: при очереди в 1 слот доля не округляется в ноль (иначе
  молчаливый отказ всем).
- цена размена по NAT закреплена явно: третий одновременный вход с того же
  адреса получает 429 при двух занятых слотах из четырёх.

Соседям по NAT стало ХУЖЕ, и теперь это записано в docstring с числами: при
флуде 3 запроса/с свои входят 69% попыток против 94% до правки, порог отказа
падает с ~14 до ~7 запросов/с. Взамен вход с чужих адресов идёт 100% против
37%. Размен сознательный, а не побочный эффект.

Refs #2714
2026-08-06 15:38:18 +05:00
3d32a0ffc7 fix(tradein/auth): доля слотов сверки пароля на адрес — потолок перестаёт бить по своим (#2714)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 3m10s
Потолок темпа из #2665 держал слоты общим котлом: флуд занимал все четыре, и
легитимный вход с ВЕРНЫМ паролем получал 429 столько раз, сколько пытался
(замер на стенде с настоящим bcrypt: 20 попыток → 0×200, 20×429).

Теперь ключ (у единственного вызывающего — IP клиента) не берёт больше половины
ёмкости: сколько бы один источник ни слал, вторая половина остаётся тем, кто
приходит впервые. Учёт по ключу живёт в самой `verify_password_bounded` и
отдаётся тем же `_release_verify_slot` — инвариант «одна точка выноса = одна
точка учёта» не делится надвое.

Граница применимости честная и записана в коде: ключом может быть только IP, а
он подделывается за вторым прокси, разделяется за NAT и ротируется ботнетом.
Это поднимает стоимость атаки, но не закрывает её; принципиально закрывают
только доказательство работы или второй фактор.

Замер на стенде после правки: 20 попыток → 5×200, 15×429, и НИ ОДНОГО 429 от
потолка сверок. Оставшиеся 15 — `_LOGIN_LIMITER` (5 попыток на пару имя+IP за
300с), отдельная и намеренная защита, срабатывающая независимо от флуда.

Тесты: вход с другого ключа во время флуда (API-уровень, мерит заявленное — не
латентность и не число попыток атакующего), доля слотов на ключ, возврат слота
при исключении в сверке, autouse-сторож «слотов занято 0» после каждого теста.
Формулировка «вход не заблокирован совсем» в тесте темпа поправлена: она про
попытки атакующего и ничего не говорит про живого пользователя.

Refs #2714, #2665, #2712
2026-08-06 14:38:19 +05:00