gendesign/tradein-mvp/backend/app/core/log_scrub.py
bot-backend 55a67e5c45
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 12s
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 5m38s
fix(tradein): маскировать секрет и в параметрах с префиксом (#3154)
Ревью-minor: якорь [?&] вплотную к имени пропускал client_secret=,
refresh_token=, auth_token=, webhook_secret=. Разрешаем префикс [\w.-]*
перед альтернацией — маскируем имена, ОКАНЧИВАЮЩИЕСЯ на чувствительное
слово. Кейс not_a_secret_name заменён на честные отрицательные:
?secretary= и ?tokens_page= (не оканчиваются на secret/token) плюс
/api/v1/token-info в пути.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 23:03:16 +05:00

72 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Секреты из query-строки не попадают в лог процесса (#3154).
Прод-факт: uvicorn пишет в access-log ПОЛНЫЙ путь вместе с query, а лог уезжает
в Loki (ретенция 30 суток, доступ по входу в Grafana):
INFO: 172.18.0.3:60322 - "POST /api/v1/trade-in/ops/glitchtip-webhook
?secret=<64 hex> HTTP/1.1" 200 OK
Скруббер в Alloy (#3115) это не ловит: там одно выражение под форму
``scheme://user:pass@host`` (DSN postgres_exporter, #3114). Чиним в СВОЁМ
процессе — тогда секрета нет и в `docker logs`, до отправки куда-либо.
Фильтр вешается на логгер (`logging.Filter`), а не на форматтер: uvicorn.access
кладёт путь в ``record.args``, до форматирования он уже там. Поэтому берём
``record.getMessage()`` и, если что-то замаскировали, подменяем msg/args.
"""
from __future__ import annotations
import logging
import re
# Имена параметров, значение которых маскируем: имя ОКАНЧИВАЕТСЯ на чувствительное
# слово, поэтому перед альтернацией допускаем префикс (`client_secret`,
# `refresh_token`, `webhook_secret`). Значение — до следующего `&`, пробела или
# кавычки (access-строка uvicorn обрамляет запрос кавычками).
_SENSITIVE_QUERY = re.compile(
r"([?&][\w.-]*(?:secret|token|api[-_]?key|apikey|access[-_]?token|password|signature|sig)=)"
r"[^&\s\"'<>]+",
re.IGNORECASE,
)
def scrub_query_secrets(text: str) -> str:
"""Заменяет значения чувствительных query-параметров на ``***``.
Имя параметра и остальная строка сохраняются — иначе access-лог перестал бы
годиться для диагностики.
"""
return _SENSITIVE_QUERY.sub(r"\1***", text)
class QuerySecretFilter(logging.Filter):
"""Маскирует секреты в query-строке ЛЮБОЙ записи логгера, к которому привязан."""
def filter(self, record: logging.LogRecord) -> bool:
message = record.getMessage()
scrubbed = scrub_query_secrets(message)
if scrubbed != message:
record.msg = scrubbed
record.args = ()
return True
def install_query_secret_filter(*logger_names: str) -> None:
"""Вешает фильтр на access-лог uvicorn И на обработчики корневого логгера.
Двумя местами, потому что uvicorn в своём log-config ставит `uvicorn.access`
собственный handler с ``propagate = False`` — до корневого его записи не
доходят. А фильтр на handler'ах корня закрывает всё остальное приложение
(записи дочерних логгеров фильтры родителя не проходят, фильтры handler'а
проходят).
Идемпотентно: повторный вызов не наплодит дублей.
"""
targets: list[logging.Logger | logging.Handler] = [
logging.getLogger(name) for name in logger_names or ("uvicorn.access",)
]
targets.extend(logging.getLogger().handlers)
for target in targets:
if not any(isinstance(f, QuerySecretFilter) for f in target.filters):
target.addFilter(QuerySecretFilter())