Перейти к содержимому

Ссылка недействительна: почему токен восстановления пароля открывает почтовый сканер

"Ссылка недействительна" с первой попытки: токен успели открыть до пользователя

В службу поддержки начали поступать странные обращения. Пользователь запрашивал восстановление пароля, получал письмо, переходил по ссылке и сразу видел сообщение: "Ссылка недействительна или устарела". Повторная отправка письма не помогала: новый токен тоже оказывался использованным ещё до того, как человек успевал открыть сообщение.

Проверка срока действия сначала не выявила проблем. Токены хранились в базе с отметкой времени, серверные часы были синхронизированы, а установленного часа вполне хватало для перехода по ссылке. Однако анализ показал важную деталь: токен уже был помечен использованным за несколько секунд до первого пользовательского запроса.

Кто открывает ссылку вместо пользователя

Между отправителем письма и почтовым ящиком получателя часто работает защитный шлюз - Secure Email Gateway. Он проверяет входящие сообщения на наличие вредоносных вложений и опасных адресов. Для этого система автоматически переходит по ссылкам из письма и анализирует ответ сервера.

Похожим образом действуют антивирусы, почтовые клиенты, мессенджеры и сервисы с функцией безопасного просмотра ссылок. Иногда проверка выполняется при доставке письма, ещё до того, как его увидел адресат. В других случаях адрес анализируется в момент клика, но пользователь всё равно может столкнуться с той же проблемой.

С технической точки зрения такой переход выглядит как обычный HTTP-запрос GET. Проверяющий сервис может использовать настоящий браузер, выполнять JavaScript и отправлять стандартный User-Agent. Поэтому определить по одному заголовку, что запрос поступил от сканера, а не от человека, практически невозможно.

Почему увеличение срока жизни токена не помогает

Первое очевидное решение - увеличить срок действия ссылки. Но если защитная система открывает URL сразу после доставки письма, то токен будет израсходован независимо от того, действует он час, сутки или неделю.

Фактически проблема заключается не во времени жизни токена, а в том, что безопасная проверка ссылки выполняет операцию, которая изменяет состояние аккаунта. Сканер не знает, что GET-запрос запускает восстановление доступа, и воспринимает его как обычную страницу.

Второй вариант - разрешить несколько переходов по одной ссылке. Это снижает безопасность механизма. Одноразовый токен предназначен для того, чтобы его нельзя было использовать повторно. Если любой, кто получил URL, может обращаться к нему несколько раз, ценность такого секрета уменьшается.

Ещё один подход - блокировать известные адреса сканеров или фильтровать запросы по User-Agent. Однако базы IP-диапазонов неполны и быстро меняются. Кроме того, проверку могут выполнять корпоративные антивирусы, почтовые сервисы и мессенджеры, чьи адреса заранее неизвестны. User-Agent также легко подделывается.

Правильная архитектура: GET только показывает, POST выполняет

Надёжный принцип давно известен: переход по ссылке не должен менять данные. GET-запрос обязан лишь показать страницу и проверить, что токен существует и ещё не истёк.

После перехода пользователь видит форму для установки нового пароля. Само действие выполняется только после отправки формы методом POST. Именно обработчик POST должен:

1. проверить токен;
2. убедиться, что он не просрочен и не использован;
3. проверить новый пароль на соответствие политике безопасности;
4. изменить пароль;
5. атомарно пометить токен использованным.

До отправки формы токен должен оставаться действительным. Тогда автоматический сканер получит страницу, но не сможет завершить операцию.

Критически важные детали реализации

В базе лучше хранить не сам токен, а его криптографический хеш, например SHA-256. В ссылку отправляется случайное значение достаточной длины, а сервер сохраняет только результат его хеширования. При утечке таблицы злоумышленник не получит готовые рабочие URL.

Проверку нового пароля необходимо выполнить до расходования токена. Если сначала отметить ссылку использованной, а затем обнаружить, что пароль слишком короткий или не соответствует требованиям, пользователь потеряет единственный способ восстановления доступа. В результате появится новая ошибка: токен формально был принят, но пароль так и не изменился.

Отдельное внимание нужно уделить конкурентным запросам. Нельзя сначала прочитать запись, убедиться, что `used = false`, а потом отдельной командой изменить значение. Два одновременных запроса могут пройти проверку и оба выполнить операцию.

Безопаснее использовать условное обновление:

```sql
UPDATE tokens
SET used = true
WHERE id = ? AND used = false;
```

После этого приложение должно проверить, была ли изменена строка. Если обновление не затронуло запись, значит токен уже использован или недействителен. Такой подход делает одноразовость реальной, а не декларативной.

Где применяется этот принцип

Проблема характерна не только для восстановления пароля. Та же ошибка встречается в ссылках для:

- подтверждения электронной почты;
- принятия приглашения в проект;
- присоединения к рабочей группе;
- подтверждения смены адреса;
- отмены подписки;
- активации аккаунта;
- согласия на важное действие;
- подтверждения платежных или административных операций.

Любая ссылка из письма может быть открыта автоматической системой. Поэтому нельзя считать факт перехода достаточным доказательством намерения пользователя.

Что проверить в собственном сервисе

Начать аудит стоит с журналов. Нужно сопоставить время генерации токена, первый запрос к URL, IP-адрес, User-Agent и момент фактического изменения данных. Если токен расходуется через несколько секунд после отправки письма, а пользователь обращается позже, это сильный признак автоматической проверки.

Также полезно проверить, какие HTTP-методы используются для критических действий. Если изменение пароля, подтверждение адреса или отписка выполняются через GET, архитектуру следует пересмотреть.

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

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

Как сделать восстановление устойчивым

Если операция особенно чувствительна, можно добавить дополнительное подтверждение: ввод кода из письма, повторный ввод адреса, подтверждение в уже авторизованном приложении или требование установить новый пароль через отдельную форму. Однако такие меры не заменяют корректное разделение чтения и изменения состояния.

Важно также продумать повторную отправку писем. Новый запрос должен либо инвалидировать старые токены, либо использовать понятную политику приоритета последнего сообщения. Пользователь должен понимать, какая ссылка актуальна, но при этом автоматическая проверка одного письма не должна ломать все последующие попытки.

Главный вывод прост: GET должен оставаться безопасным и не иметь побочных эффектов. Письмо, браузер, антивирус или почтовый шлюз могут открыть ссылку без участия человека. Поэтому токен следует считать подтверждённым только после явного действия пользователя - отправки формы, ввода кода или другого запроса, который действительно меняет состояние системы.

Прокрутить вверх