fix(tradein/auth): отказ по насыщению — до выборки из БД и с агрегированным следом (#2715) #2734
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2734
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/2715-auth-hardening-tail"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Пункты 1 и 2 хвоста #2715 (после #2712/#2717). Пункт 3 — отдельным PR #2735 (тест-сторож, файлы не пересекаются, порядок мержа любой). Пункт 4 закрыт проверкой без правки — см. ниже.
1. Атака не оставляла следа в аудите, а единственный след выселял прочие логи.
Отказ при насыщении намеренно не пишет
login_failedи не тратит бюджет неудач по имени (иначе насыщением блокируют чужую учётку) — значит инцидент был виден только строкойlogger.warningНА КАЖДЫЙ отказ, в общем и ограниченном логе бэкенда (json-file max-size 20m × max-file 3, перепровереноdocker inspectсегодня). Теперь на окно в 1с — одна запись в лог И одно событиеlogin_verify_saturatedвuser_events, обе с числом отказов с прошлой записи. Первый отказ отчитывается сразу (одиночная аномалия обязана быть видна). Имя в событии пустое намеренно: отказ случился до того, как мы на имя посмотрели, а запись присланного дала бы атакующему строки аудита с любым именем на выбор.Запись идёт на ERROR, а не WARNING: бэкенд поднят с
LoggingIntegration(level=INFO, event_level=ERROR)(app/main.py), то есть событием GlitchTip запись становится ровно с ERROR, а WARNING остаётся строкой в docker-логе — тем самым следом, на бесполезность которого жалуется issue. Прецедент цены известен (#2674): монитор писал WARNING про протухшие куки, событий было ноль. Уровень закреплён тестом.2. Гейт насыщения стоял ПОСЛЕ выборки из БД.
Теперь
verify_slots_saturated(key)— тот же предикат, что решает отказ, но без взятия слота — вызывается ДОget_user_by_username. Авторитетная проверка осталась внутриverify_password_bounded, и она зовёт ЭТУ ЖЕ функцию: двум условиям разъехаться нечем, инвариант «одна точка выноса = одна точка учёта» цел. Предчек учитывает и общий потолок, и долю на ключ (#2714) — при флуде с одного адреса первой упирается именно доля, без неё предчек не покрывал бы главный случай.Замеры
Флуд 100 соединений × 1с, у каждого запроса своё имя и свой адрес, джиттер 1-10мс. Проба прогнана и на
origin/main— там обязана показать поломку, показала:Мутационная проверка (каждая ветка ломалась по одной, тест обязан краснеть — краснел):
под насыщением всё-таки сходили в реестр: ['alice', 'ghost']20 отказов дали 20 строк в логе — агрегации нетassert {'rejected': 1} == {'rejected': 20}assert 30 == 40test_one_key_cannot_take_more_than_its_share(#2714)Что этот PR НЕ делает
login_verify_saturatedвиден запросом кuser_events(индекс поevent_typeесть, проверено на проде), но НЕ виден в админ-UI: drilldown аудита ходит поusername, а имени у этого события намеренно нет.user_events.event_type— свободныйtextбез CHECK (проверено на проде\d user_events)._SATURATION_REPORT_WINDOW_Sохраняется ЛИТЕРАЛОМ в тесте, а не арифметикой от самой настройки.Пункт 4 (утечка слота при закрытом цикле) — закрыт без правки
Autouse-фикстура из #2717 (
tests/conftest.py::_no_leaked_password_verify_slots) этот путь покрывает. Проверено пробником: тест, который занимает слот и отдаёт освобождение в ЗАКРЫТЫЙ цикл (_schedule_verify_slot_release(dead_loop, key)), падает в teardown стест оставил 1 занятых слотов проверки пароля (по ключам: {'203.0.113.77': 1}), а следующий тест видит чистое состояние — сброс до assert работает, каскада нет. Пробник удалён, в PR его нет.Test plan
pytest tests/(весь tradein-backend) — 3757 passed, 9 skippedpytest tests/test_auth_api.py tests/test_password.py tests/test_user_events.py tests/test_audit_api.py— 92 passed, 2 skippeddocker exec tradein-backend curlна заведомо несуществующее имя (ответ прежний 401),SELECT count(*) FROM user_events WHERE event_type='login_verify_saturated'Refs #2715