Сразу вывод: ключ удобно класть после решётки не потому, что появился новый криптопротокол, а потому что браузер отбрасывает fragment, когда собирает HTTP-запрос. Проверка — сравнить адресную строку со строкой запроса в Network. Как только полную ссылку вставили в чат, тикет или историю браузера, этот слой больше не держит против того, кто видит всю строку.
Сначала одним предложением — ответ на запрос
После решётки в URL можно класть ключ, потому что RFC 9110 говорит: целевой URI запроса не включает fragment. Когда браузер просит у сервера документ, он отрезает # и всё, что дальше. Хостинг, CDN и журнал доступа по задумке видят путь и параметры после ? — но не ключ после решётки.
Заметка для разработчиков, администраторов и всех, кто перед отправкой одноразовой ссылки коллеге хочет отделить «сервер не видит» от «чат не видит». Это не инструкция к самоуничтожающейся ссылке и не повтор «как создать ссылку». Унести отсюда стоит другое: на какой слой попадают ? и #, как сверить их в Network и какой слой падает, если переслать URL целиком.
Знак вопроса уходит в HTTP, решётка по умолчанию остаётся во вкладке
В одном адресе могут быть и параметры, и фрагмент. Форма такая: https://example.com/s.html?id=abc#key. После вопросительного знака id=abc — это query. После решётки key — это fragment. Браузер читает fragment от первой # до конца строки. Если решётку нужно написать раньше как обычный символ, её кодируют как %23, иначе разбор оборвётся слишком рано.
Для запроса документа браузер собирает Host, путь и параметры. В MDN про URI fragment сказано прямо: при запросе ресурса фрагмент на сервер не отправляют; клиент разбирает его уже после получения документа. В HTML им часто крутят страницу к нужному id. На странице одноразового чтения тот же слот берут, чтобы пронести ключ, которым эта вкладка расшифрует данные локально.
Поэтому фраза «на сервере только шифротекст» держится лишь если идентификатор для поиска идёт в query, ключ расшифровки — в fragment, а скрипты страницы потом не записывают location.hash в следующий запрос. Пропустите любой из этих шагов — и решётка станет декорацией.
Спецификации описывают поведение по умолчанию, а не дополнительное шифрование
RFC 3986 §3.5 определяет fragment как идентификатор вторичного ресурса поверх основного: его толкует клиент по типу документа. RFC 9110 §7.1 добавляет: целевой URI исключает fragment из ссылки, потому что идентификаторы фрагментов оставлены клиенту. Вместе это значит: то, что после #, не входит в цель HTTP-запроса.
Отсюда же пустые журналы без #. Администратор поле не прятал: этой части в строке запроса просто не было. Посчитать по серверным логам, «на какой заголовок кликнул пользователь», не выйдет по той же причине. А вот показать по тем же логам, что «ключ до сервера не дошёл», как раз совпадает со спецификацией.
Заголовок Referer живёт по тому же правилу. В MDN про Referer сказано: в нём могут быть источник, путь и параметры, но не должно быть URL fragment, имени пользователя и пароля. Referrer Policy перед упаковкой URL в Referer обнуляет fragment. Поэтому когда страница чтения потом тянет картинку или скрипт, ключ после # по умолчанию не уходит на чужой хост. Это штатное поведение браузера, а не протокол, который придумал сайт.
Скрипты страницы fragment всё равно читают. URL.hash в MDN — это # плюс сам фрагмент; текущая страница может взять и location.hash. Страница чтения как раз этим интерфейсом достаёт ключ и вызывает Web Crypto для расшифровки AES-256-GCM. Тем же интерфейсом можно ошибиться: скрипт допишет hash в query или отправит его в теле аналитики. Спецификация закрывает строку запроса, которую браузер собирает сам. Она не закрывает сценарий «страница прочитала hash и всё равно отправила».
Не ставьте опыт на настоящем пароле, API-ключе или незамаскированной строке подключения. Подготовьте одноразовую тестовую фразу. Сверяете строки запросов и тела — а не светите секрет ещё раз.
Сверьте адресную строку со строкой запроса в Network
Слоган — не доказательство. Проверку, которую можно закончить сразу, даёт панель Network: положите рядом всю строку из адреса и URL каждого запроса. В предыдущей заметке канарейкой искали открытый текст. Здесь смотрим только fragment: кусок после # должен быть в адресной строке и не должен появиться в строке запроса.
- Откройте страницу чтения и убедитесь, что адрес выглядит как
s.html?id={id}#{key}: в query только идентификатор, ключ стоит только после решётки. - Откройте Network, включите Preserve log, фильтр сначала поставьте All. Не прячьте заранее запрос документа, последующие API и аналитику.
- Вставьте ключ после
#в фильтр как канарейку. Красное совпадение — стоп: читайте этот запрос. Реализация утащила fragment в query, либо скрипт прочитал его и отправил. - Если совпадений нет, по одному откройте запрос документа и интерфейсы вроде
/secrets. В строке запроса должен быть толькоid; в теле не должно быть исходного текста.
Ту же проверку можно сделать на создании. После того как устройство зашифровало данные алгоритмом AES-256-GCM, наружу должны уйти шифротекст, срок жизни и число прочтений. Случайный ключ появляется только в ссылке, которую вы потом собираете, — не в JSON у POST. Самоуничтожающаяся ссылка FastPwd держит эту границу: создавать и читать можно сразу, аккаунт не нужен. Обещание всё равно остаётся обещанием. Верьте тому, что показала эта сессия Network.
Не путайте %23 и настоящую #. Символы после %23 в строке запроса ушли как путь или параметры — сервер их видит. Fragment начинается только с незакодированной #. Запись ключа как ?key= или /%23key может выглядеть как «спрятали в решётке». Для журнала это уже открытый query.
Скрипты страницы и аналитика всё равно могут прочитать hash
HTTP-запрос без fragment по умолчанию — это не то же самое, что «во вкладке никто его не видит». У JavaScript текущего документа есть location.hash. Страница чтения обязана его прочитать, чтобы расшифровать локально. Если скрипт аналитики отправит весь location.href, ключ окажется в query или теле этого отчёта.
Поэтому поиск канарейки нельзя заканчивать на прикладном API. Отдельно отфильтруйте путь аналитики и откройте query и body. Заголовок страницы и путь могут быть. Ключа после #, проверяемого пароля и текста до маскирования быть не должно. Аналитика посещений FastPwd идёт через собственный трекер сайта и с localhost по умолчанию не отправляется. Даже так той же канарейкой стоит убедиться, что в отчёте нет ключа из fragment.
Расширения, корпоративный DLP и часть «ИИ-браузеров» тоже умеют читать всю адресную строку. Они не едут в той HTTP-строке, которую вы смотрите, поэтому панель Network не докажет, что они остались в стороне. Сожмите утверждение до фразы, которую можно сказать коллеге: «Я искал в строке запроса и в теле аналитики — ключа после # там нет». Не раздувайте это до «на этом компьютере ни одна программа этот hash не видела».
Когда этот слой защиты ломается
Fragment закрывает сценарий «этот запрос документа отдал ключ хостингу». В случаях ниже тот, кто видит полный URL, уже держит ключ. Спецификация тут не поможет.
Первое: полную ссылку вставили в чат, письмо или тикет. В теле сообщения есть кусок после #. Чужой клиент, история комнаты, поиск по тикетам и поздний экспорт увидят всю строку. Сервер по HTTP ключ так и мог не получить. Тот, кто откроет переписку, его уже имеет. Полный URL — предъявительский секрет.
Второе: история браузера и синхронизация. Fragment с #, который мелькнул в адресной строке, обычно попадает в локальную историю. Браузер с включённой синхронизацией может унести ту же запись на другие ваши устройства. Закрыть вкладку — не значит очистить историю.
Третье: редирект может утащить fragment с собой. Если в Location своего фрагмента нет, многие браузеры дописывают исходный fragment к новому адресу. Ключ оказывается в адресной строке страницы, которую вы уже считали «безопасной». Если страницу чтения перенаправили на домен аналитики или на вход, сначала смотрите итоговую строку запроса в Network, потом — что осталось в адресе.
Четвёртое: превью ссылок и сканеры безопасности запрашивают часть до решётки. Когда мессенджер строит превью, fragment по спецификации не должен уйти на просматриваемый хост, но запрос s.html?id=… уйдёт. Если реализация сжигает запись на первом GET документа или первом GET шифротекста, бот превью может потратить прочтение раньше, чем получатель нажмёт ссылку. Получатель увидит «уже сожжено». Ключ превью-хосту не отдавали — сгорел счётчик прочтений, а не секретность fragment.
Страница чтения FastPwd эти два шага разводит: при открытии она только спрашивает, на месте ли ещё шифротекст, и сам шифротекст не забирает. После того как получатель нажмёт «открыть и посмотреть», страница берёт шифротекст по id и расшифровывает его локально ключом после #. Ни создание, ни чтение не требуют аккаунта. Боты превью обычно тянут посадочную страницу и кнопку за вас не нажимают — это снижает шанс «превью равно сожжению», но не гарантирует, что любой сканер остановится на лендинге. Счётчик всё равно серверный, в fragment он не входит.
| Кто смотрит | Видит ли ключ после # | Держится ли этот слой |
|---|---|---|
| Строка запроса документа | По умолчанию нет | Да; сверьте в Network |
| Журнал хостинга / CDN | По умолчанию нет | Да, если ключ не попал в query |
| Referer на чужой хост | По умолчанию нет | Да; спецификация срезает fragment |
| Скрипт страницы / аналитика | Может прочитать location.hash |
Зависит от реализации; ищите канарейку в теле |
| Чат, письмо, тело тикета | Видит всю строку | Против того, кому переслали, не держится |
| История браузера / синхронизация | Обычно да | Против того, кто откроет этот браузер, не держится |
| Боты превью ссылок | Fragment по спецификации не получают | Могут потратить прочтение |
Что вы можете доказать — и чего не можете
Эта проверка поддерживает узкий вывод. Она поддерживает: в этом браузере, при этом открытии, запрос документа и строки запросов интерфейсов, которые вы успели увидеть, не унесли ключ после #, а в теле запроса создания не было исходного текста.
Она не поддерживает: администратор чата никогда не сохранял полную ссылку; расширение никогда не читало адресную строку; получатель не сделает снимок экрана; шифротекст после выдачи больше нигде не всплывёт в другой резервной копии. Самоуничтожение удаляет шифротекст на сервере. Оно не удаляет открытый текст, уже расшифрованный на экране, и не удаляет полный URL из переписки.
Не читайте fragment как «открыть может только тот, кому вы хотели». У кого вся строка ?id=…#ключ, тот расшифрует на открытой странице чтения. На странице чтения нет входа. Так и задумано: другому человеку не нужно сначала регистрироваться. Цена в том, что всю строку надо хранить как секрет, а не как обычную ссылку на документ, которую можно кинуть в общий чат.
Частые вопросы
Уходит ли на сервер то, что стоит в URL после решётки?
По умолчанию нет. RFC 9110 говорит: целевой URI не включает fragment. Когда браузер запрашивает документ, он отрезает решётку и всё после неё. Хостинг, обратный прокси и журнал доступа по задумке видят только путь и параметры после ?. Если ключ после # появился в строке запроса — это ошибка реализации, а не исключение в спецификации.
Почему ключ расшифровки нельзя класть после знака вопроса?
Параметры после ? записываются в строку HTTP-запроса. Их видят хостинг, CDN и журналы доступа. Как только ключ оказался в query, фраза «на сервере только шифротекст» на уровне логов уже неверна. Идентификатор в query быть может: им как раз забирают шифротекст. Ключу там места нет.
Безопасно ли вставлять в чат полную ссылку с решёткой?
Сервер по-прежнему не видит ключ в этом HTTP-запросе. История чата, тикеты и журнал браузера — видят. Полный URL — предъявительский секрет: у кого вся строка, тот расшифрует на странице чтения. Если нужна более жёсткая изоляция, ссылку с id и ключ после # отправьте по двум каналам — не кидайте всю строку в одну комнату.
Нужен ли аккаунт, чтобы открыть страницу чтения Burn-Link?
Нет. Страница чтения открыта получателю: шифротекст берётся по id из query, ключ после # расшифровывает его на этом устройстве. У FastPwd нет аккаунтов и нет хранилища паролей. Создавать и читать можно сразу. Вход не чинит границу fragment и не должен попадать в эту проверку.
Проверьте на безвредной тестовой ссылке
Если нужна страница, где query и fragment уже разведены, начните с самоуничтожающейся ссылки FastPwd. Открыли — и можно пользоваться, регистрация не нужна. Напишите одноразовую тестовую фразу и получите локально зашифрованную ссылку. В запросе создания должны быть шифротекст плюс поля срока и числа прочтений. Вид шаринг-ссылки — s.html?id={id}#{key}.
Откройте этой строкой страницу чтения. В адресной строке должны быть и ?, и #. В Network запрос документа и проверка статуса должны нести только id. После нажатия «открыть и посмотреть» запрос, который забирает шифротекст, по-прежнему не должен тащить ключ после #. Открытый текст появляется только на странице после локальной расшифровки. Алгоритм — AES-256-GCM, считает Web Crypto. Один текстовый фрагмент — не больше 32 КБ. Целый файл лучше отдать в шифрование файлов: один файл до 5 ГБ, на выходе .lock / .enc.
Смысл упражнения не в том, чтобы доказать, что какой-то сайт «абсолютно безопасен». Смысл — держать два факта отдельно. Первый слой: хостинг по спецификации не видит fragment. Второй: кто видит полную ссылку, тот держит ключ. На любой странице, которая пишет «ключ стоит после решётки», шаги те же: сверьте адрес со строкой запроса, отдельно откройте аналитику и спросите себя, попадёт ли этот URL в переписку.
Перед следующей отправкой оставьте себе три строки. Первое: идентификатор может идти в query, ключ — только в hash. Второе: чистая строка запроса значит лишь то, что сервер ключ не получил. Третье: полную ссылку храните как секрет, а не пересылайте как обычный документ. В собственную памятку по передаче стоит писать только то наблюдение, которое можно повторить.
Что ещё почитать
Предыдущая заметка превращает вопрос «покинул ли открытый текст эту вкладку» в проверку, которую можно повторить в Network. Читайте рядом с этой: Как проверить, что локальное шифрование в браузере не отправило открытый текст. Здесь мы только разводим ? и # и называем слой, который падает, если переслать ссылку целиком.