fix(tradein/payments): тело нотификации не течёт в мониторинг и аудит, повторы банка не отбиваются лимитом #2794

Merged
lekss361 merged 2 commits from feat/tradein-payments-perimeter-hardening into main 2026-08-15 19:48:20 +00:00
Owner

PR-D2 платёжного контура. Ничего наружу не открывает — только чинит то, что порвётся в момент открытия публичного эндпоинта нотификации (PR-D3).

1. Тело платёжной нотификации утекло бы в мониторинг целиком

Ключ мониторинга на проде задан, библиотека кладёт полное тело запроса в событие, и флаг «не отправлять персональные данные» это не гейтит — он управляет только куками. Первая же ошибка в обработчике отправила бы наружу Token, Pan, ExpDate, CardId, RebillId и DATA.

Тело вырезается целиком для платёжного сегмента пути. Набор скрабируемых ключей расширен: customer_email, customer_phone, pan, expdate, cardid, rebillid, token, terminalkey.

Найдено три точки инициализации мониторинга, а не одна: основной API, планировщик скрапера и телеграм-мост. Во всех трёх был обработчик канала ошибок и ни в одной — обработчик транзакционного канала. Теперь один и тот же обработчик проведён в оба канала во всех трёх точках.

Это прямой урок вчерашнего дня: в соседнем продукте закрыли только канал ошибок, транзакционный остался с очисткой лишь по адресу, и утечка сохранилась при включённой пятипроцентной выборке. Здесь та же ошибка исключена по построению.

Вырезание тела применено и к планировщику с ботом, хотя там нет веб-запросов — ради единообразия: PR-E добавит подтверждение платежей и сверку именно в контейнер планировщика.

2. Ограничитель частоты не отбивает повторы банка

Для точного пути нотификации — собственный щедрый ограничитель (3000 запросов в минуту на адрес) по идиоме, уже применённой в поддержке, а не полное отключение.

Мотив не «банк упрётся в лимит», а «отказ по частоте никогда не должен стать причиной, по которой денежное состояние разъехалось»: для банка это «доставка не удалась», альтернативного канала у нотификации нет, а очередь повторов растягивается на сутки. При этом полное отключение оставило бы путь без всякой защиты от шторма — подпись проверяется уже после разбора тела.

Оформление заказа под общий лимит не попадает: его инициирует пользователь с сессией, там злоупотребление ограничивается штатно.

Число выбрано как «на два порядка выше проверочных 400 в минуту». Точное расписание повторов банка не задокументировано — спека помечает это как непроверенное. Правится в одном месте.

3. Тело нотификации не попадает в журнал аудита

Точный путь добавлен в набор пропускаемых аудитом.

Отступление от буквы задания, раскрываю честно: я запретил трогать _PUBLIC_PATHS, а автор его тронул — но в репозитории два разных набора с таким именем. Один в модуле прав, он действительно открывает пути и не тронут. Второй в модуле аудита — это список пропускаемых записей, к авторизации отношения не имеет, и именно он требовался по пункту 3. Трактовка верная.

Проверка

  • 64 целевых теста, полный набор — 4020 passed, 16 skipped, 0 failed;
  • отдельный тест на проводку обоих каналов во всех трёх точках — он же упадёт при появлении четвёртой точки инициализации;
  • проверка проводки сделана разбором синтаксического дерева, а не поиском подстроки: подстрока ложно совпадает с комментарием, а комментарии там как раз содержат нужные слова;
  • 400 запросов к нотификации с одного адреса за минуту не дают ни одного отказа по частоте;
  • скрабер вычищает контакты покупателя на произвольной глубине вложенности.

Канарейки в смоуке периметра

Добавлены четыре негативные проверки. Сейчас они зелёные (публичный домен отдаёт 404 — Caddy не проксирует; основной домен отдаёт 401 — права не открыты) и осознанно покраснеют, когда PR-D3 и PR-D4 откроют пути. Тогда ожидания надо обновить вместе с ними — это сигнал «периметр изменился», а не поломка.

Что осталось на следующие PR

Открытие путей в правах и в Caddy — PR-D3 и PR-D4. Этот PR ничего не открывает: маршрута нет, права всё равно отдают 401.

PR-D2 платёжного контура. **Ничего наружу не открывает** — только чинит то, что порвётся в момент открытия публичного эндпоинта нотификации (PR-D3). ## 1. Тело платёжной нотификации утекло бы в мониторинг целиком Ключ мониторинга на проде задан, библиотека кладёт полное тело запроса в событие, и флаг «не отправлять персональные данные» это **не** гейтит — он управляет только куками. Первая же ошибка в обработчике отправила бы наружу `Token`, `Pan`, `ExpDate`, `CardId`, `RebillId` и `DATA`. Тело вырезается целиком для платёжного сегмента пути. Набор скрабируемых ключей расширен: `customer_email`, `customer_phone`, `pan`, `expdate`, `cardid`, `rebillid`, `token`, `terminalkey`. **Найдено три точки инициализации мониторинга, а не одна:** основной API, планировщик скрапера и телеграм-мост. Во всех трёх был обработчик канала ошибок и **ни в одной** — обработчик транзакционного канала. Теперь один и тот же обработчик проведён в оба канала во всех трёх точках. Это прямой урок вчерашнего дня: в соседнем продукте закрыли только канал ошибок, транзакционный остался с очисткой лишь по адресу, и утечка сохранилась при включённой пятипроцентной выборке. Здесь та же ошибка исключена по построению. Вырезание тела применено и к планировщику с ботом, хотя там нет веб-запросов — ради единообразия: PR-E добавит подтверждение платежей и сверку именно в контейнер планировщика. ## 2. Ограничитель частоты не отбивает повторы банка Для точного пути нотификации — собственный щедрый ограничитель (3000 запросов в минуту на адрес) по идиоме, уже применённой в поддержке, **а не полное отключение**. Мотив не «банк упрётся в лимит», а «отказ по частоте никогда не должен стать причиной, по которой денежное состояние разъехалось»: для банка это «доставка не удалась», альтернативного канала у нотификации нет, а очередь повторов растягивается на сутки. При этом полное отключение оставило бы путь без всякой защиты от шторма — подпись проверяется уже после разбора тела. Оформление заказа под общий лимит **не** попадает: его инициирует пользователь с сессией, там злоупотребление ограничивается штатно. Число выбрано как «на два порядка выше проверочных 400 в минуту». Точное расписание повторов банка не задокументировано — спека помечает это как непроверенное. Правится в одном месте. ## 3. Тело нотификации не попадает в журнал аудита Точный путь добавлен в набор пропускаемых аудитом. **Отступление от буквы задания, раскрываю честно:** я запретил трогать `_PUBLIC_PATHS`, а автор его тронул — но в репозитории **два разных** набора с таким именем. Один в модуле прав, он действительно открывает пути и не тронут. Второй в модуле аудита — это список пропускаемых записей, к авторизации отношения не имеет, и именно он требовался по пункту 3. Трактовка верная. ## Проверка - 64 целевых теста, полный набор — **4020 passed, 16 skipped, 0 failed**; - отдельный тест на проводку **обоих каналов во всех трёх точках** — он же упадёт при появлении четвёртой точки инициализации; - проверка проводки сделана разбором синтаксического дерева, а не поиском подстроки: подстрока ложно совпадает с комментарием, а комментарии там как раз содержат нужные слова; - 400 запросов к нотификации с одного адреса за минуту не дают ни одного отказа по частоте; - скрабер вычищает контакты покупателя на произвольной глубине вложенности. ## Канарейки в смоуке периметра Добавлены четыре негативные проверки. Сейчас они зелёные (публичный домен отдаёт 404 — Caddy не проксирует; основной домен отдаёт 401 — права не открыты) и **осознанно покраснеют**, когда PR-D3 и PR-D4 откроют пути. Тогда ожидания надо обновить вместе с ними — это сигнал «периметр изменился», а не поломка. ## Что осталось на следующие PR Открытие путей в правах и в Caddy — PR-D3 и PR-D4. Этот PR ничего не открывает: маршрута нет, права всё равно отдают 401.
lekss361 added 1 commit 2026-08-07 13:19:48 +00:00
fix(tradein/payments): тело нотификации не течёт в мониторинг и аудит, повторы банка не отбиваются лимитом
All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Successful in 3m50s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
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
1f85ef7d4e
PR-D2 платёжного контура МЕРЫ — закрывает утечки до открытия публичных путей
(PR-D3/D4), сам ничего не открывает: _PUBLIC_PATHS (rbac.py), Caddyfile,
roles.yaml, auth_session.py не тронуты.

- sentry_scrub.py: новая scrub_payment_request_body — вырезает
  event.request.data целиком для /api/v1/trade-in/payments/* (sentry_sdk 2.64
  кладёт полное тело запроса в request.data, send_default_pii=False это НЕ
  гейтит — тот флаг управляет только куками). Плюс расширен _PII_KEYS:
  customer_email/customer_phone/pan/expdate/cardid/rebillid/token/terminalkey.
- main.py, scheduler_main.py, tgbot_main.py (все 3 точки инициализации
  sentry_sdk.init в проекте) — тот же обработчик проведён в ОБА канала,
  before_send и before_send_transaction. Мотивирующий инцидент: на соседнем
  продукте вчера закрыли только error-канал, transaction остался без
  обработчика вообще.
- ratelimit.py: точный путь notify — свой щедрый SlidingWindowLimiter
  (3000/60с per-IP, идиома support.py) вместо общего лимитера, но НЕ полное
  отключение — backstop против шторма запросов остаётся, подпись проверяется
  уже после разбора тела (PR-D3). Только notify, не checkout (тот с сессией).
- request_audit.py: notify — в audit skip-набор (defense-in-depth: middleware
  внешний относительно rbac_guard и читает сырой X-Authenticated-User —
  спуфнутый заголовок иначе писал бы фальшивые события с атрибуцией admin).
- smoke-mera-perimeter.sh: негативные проверки-канарейки — notify/checkout
  сейчас закрыты 404 (meraocenka.ru, Caddy не проксирует) и 401
  (gendsgn.ru, rbac ещё не открыл) с обеих сторон периметра.

Тесты: scrub на произвольной глубине + payment-path body-wipe, AST-разбор
(не substring — комментарии в этих же файлах сами упоминают
before_send_transaction) на проводку обоих каналов во всех точках
инициализации, 400 запросов notify без единого 429 + контроль что общий
лимитер по-прежнему активен на других путях, notify вне user_events даже со
спуфнутым X-Authenticated-User: admin.
bot-backend added 1 commit 2026-08-15 19:32:39 +00:00
merge(tradein/payments): влить main в feat/tradein-payments-perimeter-hardening
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m56s
a8fa7364ae
Слияние main принесло собственные Sentry-скрубберы (redact_telegram_bot_token +
stabilize_retry_error_fingerprint) в app/main.py и app/scheduler_main.py — конфликт
разрешён композицией, а не выбором стороны: обработчик перед отправкой в GlitchTip
теперь прогоняет событие через всю цепочку в указанном порядке:
scrub_payment_request_body → scrub_pii_event → redact_telegram_bot_token →
stabilize_retry_error_fingerprint (main.py), и без redact_telegram_bot_token в
scheduler_main.py (тот процесс не держит TelegramClient) — оба канала,
before_send и before_send_transaction, используют один и тот же обработчик.

tests/test_sentry_scrub.py: тесты обеих сторон объединены без потерь — PR-D2
платёжный composed-тест (body-wipe + PII-scrub + token-redaction) и весь блок
RetryError fingerprint-стабилизации из main сосуществуют в одном файле.
lekss361 merged commit 58f04087bb into main 2026-08-15 19:48:20 +00:00
lekss361 deleted branch feat/tradein-payments-perimeter-hardening 2026-08-15 19:48:21 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2794
No description provided.