Совет про токен в адресе страницы кочует из статьи в статью в одной и той же формулировке: не кладите его в URL, иначе он уедет в заголовке Referer на все сторонние ресурсы, которые подключает ваша страница.
Совет правильный по выводу и неправильный по обоснованию. Через Referer он давно никуда не уезжает. А вот сам токен из URL утекает — просто в других местах, и там его обычно никто не ищет.
Что уходит в Referer сейчас
Проверяется это на двух HTTP-серверах. Сервер A на порту 8123 отдаёт страницу, сервер B на порту 8124 играет роль стороннего ресурса: для браузера разные порты — это разные источники. Оба сервера пишут в журнал каждый пришедший запрос вместе со значением заголовка Referer.
Страница на сервере A выглядит так:
<img src="http://127.0.0.1:8124/cross.png"> <!-- чужой источник --> <img src="/same.png"> <!-- свой источник --> <script>fetch('http://127.0.0.1:8124/xhr.json')</script>
Открываем её по адресу http://127.0.0.1:8123/reset?token=A1B2C3D4E5 и смотрим, что записали оба сервера:
сервер | запрошенный путь | полученный Referer -------+--------------------+-------------------------------------------------- A | /reset?token=A1B2… | (заголовка нет — адрес введён в строке браузера) A | /same.png | http://127.0.0.1:8123/reset?token=A1B2C3D4E5 B | /cross.png | http://127.0.0.1:8123/ B | /xhr.json | http://127.0.0.1:8123/
Сравните две последние строки с предыдущей. Своему же серверу браузер отдал полный адрес: путь /reset и параметр token со значением на месте. Стороннему серверу — строку http://127.0.0.1:8123/, в которой есть схема, хост и порт, а дальше косая черта и конец. Ни пути, ни параметров, ни токена. Это и есть проверка исходного тезиса: через межсайтовый Referer токен не уходит.
Так стало не вчера. Раньше по умолчанию действовала политика no-referrer-when-downgrade: полный адрес уходил на любой ресурс, лишь бы соединение не деградировало с HTTPS на HTTP. Chrome сменил умолчание на strict-origin-when-cross-origin в версии 85, в августе 2020-го, Firefox — в 87-й, в марте 2021-го. Safari подрезал межсайтовый Referer до источника ещё раньше, но сначала выборочно — для доменов, которые ITP считал трекерами. Сейчас поведение из старых статей нужно включать вручную, выставив Referrer-Policy: unsafe-url.
А вот строка A /same.png — это уже интересно. Своему же ресурсу браузер отдал полный адрес вместе с токеном. Политика оставляет полный адрес внутри одного источника, межсайтово отдаёт только origin, а при переходе с HTTPS на HTTP не отправляет ничего — отсюда strict- в названии.
Пять мест, где токен оседает
В ваши собственные логи. Первое место по ущербу и последнее по вниманию. Nginx с типовым combined пишет $request_uri — то есть путь вместе со всеми параметрами:
198.51.100.14 - - [14/Aug/2026:10:41:02] "GET /reset?token=<токен> HTTP/1.1" 200
Дальше эта строка живёт своей жизнью: уезжает в централизованный сбор логов, ложится в индекс, попадает под срок хранения на год, копируется в бэкапы, доступна на чтение всей смене эксплуатации и подрядчику по мониторингу. Токен сброса пароля действует пятнадцать минут, а запись о нём — год.
Причём попадает он туда дважды: в $request_uri самого запроса и в поле Referer каждого запроса, который страница делает к своему же домену, — это ровно та строка A /same.png из вывода выше.
В аналитику, и Referrer-Policy тут ни при чём. Счётчику не нужен заголовок: скрипт исполняется на вашей странице и читает адрес сам, через location.href, а потом отправляет его в теле своего запроса. То же делают системы сбора ошибок — событие в них несёт полный адрес страницы, на которой всё случилось. Referrer-Policy управляет одним конкретным заголовком исходящего запроса и дальше него не распространяется. Скрипт берёт адрес из объекта location, до всяких заголовков, и политика ему в этом не мешает.
В историю браузера и в синхронизацию профиля. Адрес с токеном сохраняется в историю, синхронизируется на остальные устройства пользователя, всплывает в подсказках адресной строки — в том числе когда человек показывает экран на созвоне.
В пересланную ссылку. Пользователь копирует адрес из строки браузера и отправляет коллеге «посмотри, у меня тут ошибка». Дальше происходит одно из двух. Либо ссылка теперь работает у коллеги — токен ушёл человеку, которому не предназначался. Либо мессенджер по дороге развернул превью, дёрнув адрес своим ботом, и одноразовый токен сгорел на этом боте: пользователь открывает ссылку и видит «срок действия истёк», не понимая почему.
В промежуточные узлы с расшифровкой TLS. Корпоративный прокси, который разбирает HTTPS, видит полный запрос и пишет его в свой журнал. Здесь HTTPS не помогает по определению.
Что делать
Не передавать токен параметром там, где есть выбор. Заголовок или тело POST-запроса не попадают ни в $request_uri, ни в историю, ни в location.href. Бесплатным это не делает: заголовки любит писать прокси, а тело запроса — системы сбора ошибок. Но каналов утечки становится на порядок меньше.
Для ссылок из письма — обменивать токен на сессию сразу. Из письма пользователь приходит по ссылке, и что-то одноразовое в ней нести придётся — параметром или сегментом пути, для логов разницы немного. Поэтому работает другая схема: страница по такой ссылке первым же действием обменивает токен на обычную сессию, гасит его и делает редирект на адрес без параметров. Токен остаётся валидным ровно на один запрос, и всё, что осело в логах и в истории, к моменту чтения уже бесполезно. Заодно имеет смысл держать короткий срок жизни: минуты, а не сутки.
Вырезать параметры из логов. В nginx достаточно логировать $uri вместо $request_uri — с оговоркой, что $uri показывает путь уже после внутренних rewrite, а не то, что просил клиент. Если параметры нужны, чувствительные чистятся через map:
# ~* — без учёта регистра, иначе TOKEN=... уедет в лог как есть map $request_uri $safe_uri { ~*^(?<path_only>[^?]*)\?.*(?:token|code|key|password|secret)= "$path_only?<вырезано>"; default $request_uri; } log_format safe '$remote_addr - - [$time_local] "$request_method $safe_uri" $status';
Два момента, на которых легко обжечься. ~* вместо ~: с регистрозависимым шаблоном TOKEN= проедет мимо фильтра целиком. И вырезается весь набор параметров, а не один чувствительный — если остальные нужны для разбора инцидентов, $safe_uri придётся собирать из отдельных $arg_*. Имя группы берите неочевидное: именованная группа заводит глобальную переменную nginx, и второй такой map с $p уронит конфигурацию.
Чистить надо оба поля, а не одно: и $request_uri, и $http_referer. Проверять — не только веб-сервер: полный адрес пишут в свои журналы и балансировщик, и сеть доставки контента, и сам сервис приложения.
Поставить Referrer-Policy: no-referrer на страницах с токеном. Межсайтовую утечку браузер закрыл сам, а эта политика закрывает ещё и внутреннюю — ту самую строку A /same.png из вывода. Особенно это важно, когда часть статики лежит на вашем же домене, но фактически обслуживается снаружи — путь вроде /static/ уходит на CDN или к подрядчику. Для браузера это тот же источник, значит, он отдаёт полный адрес с токеном, — а лог с этим адресом оказывается уже не у вас.
Проверить настройки аналитики и сбора ошибок. И в счётчиках, и в системах вроде Sentry есть возможность вычищать параметры адреса перед отправкой. По умолчанию она обычно выключена.
И не рассчитывать на политику браузера как на защиту. Она исполняется на стороне клиента. Старый встроенный браузер, приложение с собственным движком, программа, открывающая ссылку по-своему, — всё это ваши правила не читает.
Что посмотреть у себя
Взять access-логи за последнюю неделю и поискать в них token=, code=, key=, password=, access_token=. Находки означают, что эти значения уже лежат в системе хранения логов и во всех её копиях, и вопрос только в сроке хранения.
Отдельно проверить, что попадает в аналитику со страниц подтверждения, сброса пароля и приглашения в систему. Открыть такую страницу с инструментами разработчика и посмотреть, что именно уходит в теле запроса к счётчику. Полный адрес там — обычное дело, и для команды это каждый раз новость.

