Ссылка восстановления пароля обычно дает временное право назначить новый пароль без знания старого. Если полный reset token попадает в access log, журнал приложения, APM, аналитику или систему обработки ошибок, любой человек или сервис с доступом к этой записи потенциально получает то же право. Риск сохраняется до истечения срока действия токена, его использования или принудительного отзыва.
Исправление нельзя сводить к удалению одной строки из лога. Сначала нужно остановить новые утечки, определить все места, куда успели попасть ссылки, отозвать действующие токены и только затем менять схему восстановления. Ниже приведен практический порядок действий без публикации самих секретов и без разрушения данных, которые могут понадобиться для расследования.
Почему токен в логах считается утечкой
Reset token относится к bearer-секретам: система проверяет владение значением, а не личность человека, который его предъявил. Поэтому чтение такой ссылки из журнала может позволить завершить восстановление от имени владельца аккаунта. Даже если логи доступны только сотрудникам, они часто копируются в облачное хранилище, SIEM, APM, резервные копии и интерфейсы поддержки, где круг доступа становится шире.
- веб-сервер или reverse proxy записывает полный URI вместе с query string;
- приложение логирует объект запроса, параметры маршрута или исключение целиком;
- APM и распределенная трассировка сохраняют URL, атрибуты span и breadcrumbs;
- CDN, WAF, балансировщик или API-шлюз ведет собственный журнал запросов;
- аналитика получает адрес страницы как page URL;
- внешний скрипт, изображение или ссылка получает исходный адрес через Referer;
- почтовый сервис оборачивает ссылку трекером переходов;
- оператор поддержки вставляет полную ссылку в тикет или чат;
- архивы, дампы и резервные копии сохраняют уже собранные записи.
Сам факт наличия токена в журнале еще не доказывает захват аккаунта. Но он означает, что секрет вышел за предусмотренную границу хранения, поэтому его нужно считать скомпрометированным и проверить фактическое использование.
Что сделать сразу после обнаружения
Остановить запись новых секретов
Сначала включите маскирование параметра reset token в приложении и на ближайшем к нему прокси. Если адрес имеет вид /reset?token=..., в журнал должна попадать только нормализованная форма вроде /reset?token=[REDACTED]. Для APM и error tracking настройте фильтр до отправки события во внешний сервис. Временное отключение подробного логирования допустимо только для конкретного поля: полностью выключать аудит авторизации опасно.
Зафиксировать границы инцидента
- когда токены начали записываться и когда утечка была остановлена;
- какие окружения затронуты: production, staging, резервный контур;
- какие компоненты получили URL и каков срок хранения их журналов;
- сколько уникальных запросов восстановления могло попасть в записи;
- кто и какие сервисные учетные записи имели доступ к этим данным;
- были ли выгрузки, пересылки, публичные ссылки или подозрительные обращения к логам.
Не вставляйте найденные токены в общую таблицу расследования. Для сопоставления используйте внутренний идентификатор события, хеш или последние несколько безопасных символов, если это действительно необходимо. Доступ к исходным журналам ограничьте и отдельно протоколируйте.
Отозвать открытые токены
Надежнее всего инвалидировать все неиспользованные reset tokens, созданные в затронутый период. Если система хранит поколение восстановления или reset_requested_at, увеличьте поколение либо измените контрольное время так, чтобы старые ссылки перестали проходить проверку. Не полагайтесь только на короткий TTL: пока он не истек, ссылка остается рабочей.
После отзыва запросите новую ссылку на тестовом аккаунте и убедитесь, что старая возвращает одинаковую нейтральную ошибку без раскрытия состояния пользователя. При признаках использования чужой ссылки нужно действовать по процессу реагирования: проверить смены пароля, входы и сессии, уведомить владельца аккаунта и при необходимости завершить активные сессии. Массовый выход всех пользователей без оценки масштаба не всегда оправдан, но для подтвержденного захвата аккаунта он обычно нужен.
Сохранить доказательства, но ограничить доступ
Не удаляйте журналы вслепую до фиксации временного диапазона и согласования с ответственным за безопасность или владельцем системы. Сохраните защищенную копию, ограничьте чтение, зафиксируйте контрольную сумму и доступы. Рабочие индексы и архивы очищайте или переиндексируйте по утвержденной политике хранения. Так можно удалить секреты из повседневного поиска, не потеряв сведения, необходимые для расследования и юридических обязанностей.
Как найти все копии токена
Проверять нужно путь запроса от браузера до приложения и все ответвления телеметрии. Начните с тестового токена, который не связан с реальным пользователем, и проследите его прохождение. Не используйте production-секреты для поиска в интерфейсах сторонних сервисов.
- Создайте тестовый аккаунт и запросите восстановление в контролируемом окружении.
- Откройте ссылку один раз и зафиксируйте точное время, request id и trace id.
- Проверьте access и error logs веб-сервера, reverse proxy, контейнера и приложения.
- Проверьте CDN, WAF, балансировщик, API-шлюз и почтовый трекер.
- Откройте APM, трассировку, crash reporting и централизованный поиск логов.
- Проверьте веб-аналитику, тепловые карты, запись сессий и сторонние виджеты страницы восстановления.
- Проверьте архивы, экспортированные отчеты и резервные копии в пределах срока хранения.
- Убедитесь, что после маскирования новое значение нигде не появляется в открытом виде.
Поиск только по названию параметра недостаточен. Значение могло оказаться в полном URL, тексте исключения, заголовке события, поле original_url, атрибуте HTTP route или сообщении поддержки. Составьте карту систем и отметьте для каждой владельца, срок хранения и механизм удаления.
Безопасная модель токена восстановления
Рекомендации OWASP для восстановления пароля требуют криптографически случайных, достаточно длинных, одноразовых и ограниченных по времени токенов. Они должны быть связаны с конкретным пользователем и храниться безопасно. Практичная серверная модель не хранит сам секрет в открытом виде.
- token_id или selector помогает быстро найти запись без раскрытия секрета;
- secret генерируется криптографически стойким генератором и отправляется только пользователю;
- в базе хранится хеш secret, а не исходное значение;
- user_id связывает запрос с конкретным аккаунтом;
- purpose отделяет сброс пароля от подтверждения почты и других операций;
- expires_at ограничивает время действия;
- used_at или revoked_at запрещает повторное применение;
- created_at, request id и безопасный контекст помогают расследованию без записи секрета.
При предъявлении ссылки сервер находит запись по selector, вычисляет хеш полученного secret и сравнивает его с сохраненным значением функцией постоянного времени. После успешной смены пароля запись помечается использованной в той же транзакции. Новая заявка может отзывать предыдущие активные ссылки, если продукту не требуется несколько параллельных запросов.
reset request generate selector + random secret store selector + hash(secret) + user_id + expires_at email HTTPS link reset confirmation find unused record by selector verify expiry and constant-time hash comparison change password and mark token used in one transaction optionally revoke existing sessions send security notificationКак уменьшить утечку через URL и браузер
Первый GET-запрос по ссылке не должен менять пароль. Он только проверяет токен и открывает форму. После успешной проверки можно обменять URL-токен на ограниченную серверную сессию восстановления, убрать секрет из адресной строки перенаправлением и принимать новый пароль методом POST. Такая сессия должна разрешать только одну операцию, иметь короткий срок жизни и завершаться после смены пароля.
- используйте только HTTPS и формируйте ссылку из доверенного базового адреса, а не из произвольного Host;
- задайте для страницы строгую Referrer-Policy, например no-referrer;
- не подключайте на страницу восстановления рекламу, чат, session replay и лишнюю аналитику;
- не передавайте token в события аналитики, dataLayer и сообщения об ошибках;
- не помещайте секрет в заголовок страницы, DOM-атрибуты и клиентское хранилище;
- после обмена токена перенаправляйте на чистый URL без query string;
- не кэшируйте страницу и ответ с данными процесса восстановления;
- ограничьте число попыток проверки токена и запросов восстановления.
Фрагмент после символа # не отправляется серверу в обычном HTTP-запросе, но перенос всей проверки в клиентский JavaScript создает другие риски и не отменяет необходимость безопасной серверной схемы. Для большинства сайтов понятнее использовать серверный обмен одноразовой ссылки на ограниченную сессию и централизованное маскирование URL.
Что логировать вместо полного токена
События восстановления нужны для обнаружения перебора, расследования и поддержки. Удалять весь аудит не следует. OWASP рекомендует исключать из журналов access tokens, идентификаторы сессий, пароли, ключи и другие первичные секреты либо маскировать их до записи. Для процесса сброса пароля достаточно безопасных признаков.
- event_name: password_reset_requested, token_verified, password_changed или reset_failed;
- внутренний event id, request id и trace id;
- псевдонимизированный user id или стабильный хеш, если это разрешено политикой;
- результат и нормализованная причина отказа без секретных параметров;
- время, окружение, версия приложения и узел обработки;
- сокращенный сетевой контекст и user agent в объеме, оправданном задачей безопасности;
- счетчик попыток, сигнал rate limit и решение системы защиты.
Маскирование лучше реализовать несколькими слоями: типизированным логированием в коде, фильтром middleware, правилом reverse proxy и scrubber в APM. Это не лишнее дублирование, а защита от ошибок конфигурации одного компонента. При этом тест должен проверять итоговую запись в каждом фактическом хранилище.
Пример настройки журналирования
Вместо конкатенации строки запроса используйте структурированное событие с разрешенным набором полей. Полный request object, body и headers не должны автоматически прикладываться к ошибке авторизации. Для веб-сервера можно исключить query string из формата лога на маршруте восстановления или заменить значение чувствительного параметра до записи. Конкретная директива зависит от Nginx, Apache, ingress, CDN и используемого агента наблюдаемости.
{ "event": "password_reset_token_checked", "request_id": "...", "user_ref": "pseudonymous-id", "result": "expired", "route": "/reset-password", "token": "[REDACTED]" }Не маскируйте значение только при отображении в интерфейсе. Если исходный секрет уже хранится в индексе, экспорте или резервной копии, риск остается. Фильтрация должна происходить до сохранения, а старые данные обрабатываются отдельным планом очистки и ограничения доступа.
Как проверить исправление
- новая ссылка работает только до установленного expires_at;
- после первого успешного использования повторный запрос отклоняется;
- выпуск новой ссылки отзывает старую, если такова политика продукта;
- отозванные во время инцидента токены больше не принимаются;
- полный URL отсутствует в access logs, application logs, APM, CDN и аналитике;
- переход со страницы восстановления не передает секрет через Referer;
- ответ на запрос сброса одинаков для существующего и отсутствующего адреса;
- ограничение частоты срабатывает без блокировки чужого аккаунта;
- смена пароля и пометка token used выполняются атомарно;
- пользователь получает уведомление о смене пароля без самого пароля и reset token.
Повторите тест через обычный браузер, мобильный webview и почтовый трекер, если он используется. Отдельно проверьте исключения: просроченный токен, поврежденное значение, две параллельные вкладки, повторная отправка формы и сбой базы между сменой пароля и пометкой ссылки использованной.
Типичные ошибки при устранении утечки
- исправить только лог приложения, забыв про Nginx, CDN, APM и аналитику;
- дождаться истечения TTL вместо немедленного отзыва известных скомпрометированных ссылок;
- удалить журналы до фиксации масштаба и признаков возможного злоупотребления;
- хранить reset token в базе открытым текстом;
- логировать весь request body ради отладки формы нового пароля;
- использовать один и тот же токен несколько раз или не проверять used_at;
- возвращать разные ответы для зарегистрированной и неизвестной почты;
- автоматически входить в полную пользовательскую сессию сразу после открытия ссылки;
- добавлять на чувствительную страницу сторонние скрипты и запись пользовательской сессии;
- считать маскирование в интерфейсе достаточным, когда оригинал остается в хранилище.
Как предотвратить повторение
Заведите реестр чувствительных полей: password, authorization, cookie, session id, reset token, verification code, API key и другие секреты проекта. Для каждого поля определите, где оно создается, передается, хранится и маскируется. Запрет на логирование должен проверяться автоматическими тестами и применяться к новым интеграциям наблюдаемости до их запуска в production.
- добавьте тест, который отправляет уникальный canary token и ищет его во всех журналах;
- проверяйте конфигурацию логов, APM и аналитики при каждом изменении маршрута восстановления;
- ограничьте роли чтения логов и регулярно пересматривайте доступы;
- установите обоснованный срок хранения и автоматическое удаление архивов;
- контролируйте экспорт журналов и публичные ссылки в системах поддержки;
- проводите code review всех мест, где сериализуется HTTP-запрос;
- добавьте метрики запросов восстановления, ошибок проверки и успешных смен пароля без секретов;
- подготовьте процедуру массового отзыва reset tokens без аварийной правки базы.
Когда нужна помощь с восстановлением пароля
Если токен сброса пароля попадает в логи, я могу проверить полный маршрут от reverse proxy и приложения до CDN, APM и аналитики, определить масштаб, настроить маскирование, безопасно отозвать открытые ссылки и исправить модель хранения reset tokens. Для оценки пришлите стек проекта, схему восстановления, перечень систем журналирования и обезличенный пример записи без действующих токенов, паролей и персональных данных.