Что остаётся на сервере, когда одноразовую ссылку прочитали один раз

Пароль от базы в чат, код восстановления в заявку — после этого обычно спрашивают не «как называется алгоритм», а где ещё лежит эта строка. Одноразовая ссылка отвечает на вторую половину: исходного текста на сервере не было; шифротекст удаляют после успешного запроса и исчерпания счётчика. Синяя строка в истории чата может остаться. Это уже другой слой риска.

Запомните вывод, который можно проверить в одной вкладке. Первое открытие страницы чтения должно загрузить только оболочку и ещё не запрашивать шифротекст. После нажатия «Открыть и просмотреть» в Network появляется запрос по идентификатору. Когда счётчик исчерпан, тот же id отвечает 410. В адресной строке ключ после # может быть. В строке запроса этой же строки быть не должно.

«Самоуничтожающаяся ссылка» часто звучит так: текст сгорел вместе с синей строкой. Метафора смотрит не туда. Мессенджер не стирает сообщение, потому что адрес ответил 410. История браузера, исходное письмо и комментарий в заявке остаются. Сгорает короткоживущий шифротекст на хосте. Полный URL в чужих руках — всё ещё набор символов. Откройте его снова: идентификатор указывает на объект, которого уже нет.

Прежде чем спрашивать «что осталось», разделите два удостоверения. Первое — локатор: id после вопросительного знака, он говорит серверу, какую запись отдать. Второе — ключ расшифровки: фрагмент после решётки, он остаётся текущей вкладке. Хост по задумке видит первое и не видит второе. После одного успешного чтения локатор может указывать в пустое место. Если ключ всё ещё в истории чата, он ничего не расшифрует: шифротекста уже нет.

Ссылка на файл в облаке устроена наоборот. Пока она действительна, файл обычно лежит на диске. Вы отзываете доступ, а не удаляете объект при первом открытии. Одноразовая ссылка связывает «прочитать» и «удалить» с одним и тем же запросом. При создании задают число чтений (1–10, по умолчанию 1) и срок (1 час, 24 часа, 7 дней или только после прочтения). Типичный путь: первый успешный запрос — шифротекст удаляют. Не прочитали и вышел срок — удаляют так же. Ни в одном из путей нет копии исходного текста, которую можно потом достать с сервера.

Исходного текста на сервере не было никогда

«После чтения сотрут открытый текст» — вторая частая ошибка. Порядок другой. Исходный текст есть только во вкладке отправителя. Браузер через Web Crypto берёт случайный 256-битный ключ и шифрует AES-256-GCM. На выход уходят шифротекст, срок жизни и максимум чтений. Сервер возвращает непредсказуемый идентификатор. Страница дописывает ключ в s.html?id={id}#{key}. Шага «сначала положить пароль в базу, потом зашифровать» нет.

Параметры можно сверить, это не слоган. NIST SP 800-38D для GCM рекомендует IV на 96 бит (12 байт), чтобы реализации оставались совместимыми и простыми; тег аутентификации обычно 128 бит (16 байт). AesGcmParams в Web Crypto совпадает с этой формой. В MakePwd шифротекст — Base64 из 12 байт IV, самого шифротекста и 16-байтного тега. Ключ — 32 байта. Одна запись — не больше 32 КБ. Эти числа написаны на странице создания. В только что полученной ссылке после решётки лежит ключ в Base64URL, а не фраза, которую вы только что ввели.

GCM ещё даёт целостность. Если шифротекст или тег подменили, расшифровка в этой вкладке падает. «Похожего» исходного текста вы не получите. Это ловит порчу по дороге. Снимок экрана после того, как человек уже прочитал сообщение, оно не ловит. У сервера ключа нет: он не расшифрует за вас, не просмотрит текст и не сделает копию исходника после чтения. Он может отдать шифротекст по id и затем удалить его по счётчику или TTL.

Этап В браузере На сервере
Создание завершено Исходный текст, который вы только что ввели, и полная ссылка Шифротекст, идентификатор, TTL, оставшиеся чтения
Страница чтения только нарисовалась В адресной строке id и ключ после # По-прежнему шифротекст; эта отрисовка его не удалила
Подтверждение прошло, запрос успешен Исходный текст, расшифрованный в этой вкладке Удалено, если счётчик исчерпан; иначе счётчик уменьшен
Тот же id открыли снова Ключ всё ещё может быть в адресной строке Уничтожено или истекло; ответ класса 410

Идентификатор после вопроса, ключ после решётки

Одноразовая ссылка должна сделать две разные вещи: сказать серверу, какую запись отдать, и сказать браузеру получателя, каким ключом расшифровать. Эти задачи не должны делить одно HTTP-поле. Параметры после ? попадают в строку запроса: обратный прокси и журналы доступа видят их по задумке. Фрагмент после # спецификация оставляет клиенту.

RFC 9110, раздел 7.1 говорит прямо: целевой URI не содержит fragment, потому что идентификатор фрагмента обрабатывает клиент. Браузер просит у сервера s.html?id=…, а не всю адресную строку. Переложите ключ в ?key= — и обещание «на сервере только шифротекст» уже ложно на уровне логов: админ, открывший сегодняшний access log, видит вторую половину, нужную для расшифровки.

Предыдущая заметка разбирает этот слой подробно. Здесь граница короткая: разделение прячет ключ от хоста. Оно не делает полную строку безопасной для пересылки. Вставьте URL с # в чат — ключ увидят и получатель, и сам сервис переписки. Фрагмент закрывает HTTP. Буфер обмена он не закрывает. Шаги, которые можно повторить в Network, — в заметке Почему ключ после решётки в URL удобно класть и когда эта защита ломается.

Сгорает не первая отрисовка, а запрос шифротекста

Открыть страницу чтения и потратить одно чтение — разные события. HTML может сначала нарисоваться: проверить, что в адресе есть ?id= и ключ после #, и остановиться на кнопке подтверждения. Этот шаг тратит только статику. Запрос, после которого сервер удаляет шифротекст, приходит позже — по идентификатору. Нажали кнопку, запрос прошёл, счётчик исчерпан — запись из «ещё можно забрать» становится «уничтожено».

Без ключа запрашивать нельзя. Если в адресе только id — мессенджер обрезал решётку — страница должна сказать, что фрагмента нет, а не сразу просить шифротекст. Иначе вы сожжёте единственное чтение: сервер отдаст и удалит, браузер не расшифрует, отправитель решит, что получатель уже прочитал. Проверяемое поведение: без # в Network нет запроса шифротекста; при полной ссылке и нажатии «Открыть и просмотреть» — есть.

Счётчик не всегда равен единице. При создании можно поставить от 1 до 10. Если стоит 3, после двух успешных запросов шифротекст ещё на сервере, уменьшилось только оставшееся число. Третий успех удаляет запись. Единица по умолчанию подходит задаче «передать один пароль», а не потому что протокол умеет только одно чтение. TTL — отдельное условие удаления: не прочитали за 24 часа — шифротекст тоже исчезает, и состояние должно отличаться от «уже прочитали». Страница чтения MakePwd в ответе 410 трактует expired как истечение срока; остальные сожжённые состояния — как уже уничтоженные.

Не проверяйте на настоящем пароле, боевом API-ключе или несмаскированной строке подключения. Возьмите одноразовую канарейку, например canary-burn-2026-do-not-reuse. Смотрите код ответа и строку запроса, а не разносите ещё один секрет.

410 и 404: уничтожено, истекло или никогда не существовало

Когда шифротекста уже нет, сервер всё равно должен ответить на следующий запрос. RFC 9110, раздел 15.5.11 пишет 410 Gone так: целевой ресурс больше недоступен на сервере-источнике, и это состояние, скорее всего, постоянно. Если источник не может сказать, постоянно ли оно, лучше отдать 404. Страница MDN про 410 добавляет: клиенту не стоит повторять запрос, а сайту стоит убрать ссылки, которые всё ещё ведут на этот ресурс.

Для одноразовой ссылки 410 точнее, чем 404: этот id когда-то называл объект с шифротекстом, объект удалили намеренно, и он не вернётся. И истечение срока, и исчерпанный счётчик могут отвечать 410; тело ответа отделяет expired от обычного уничтожения, чтобы получатель не решил, что ошибся в ссылке. 404 всё равно бывает: неверный формат id или запись сняли так, что хост уже не утверждает, будто она существовала. Для отправителя оба ответа значат одно: с сервера содержимое не вернуть.

410 ещё говорит краулерам не возвращаться. Страница чтения — временная посадка для шифротекста. Для поисковиков она должна быть noindex и не попадать в sitemap. Индексировать страницу с одноразовым паролем не нужно. Страницу создания можно индексировать: она объясняет, как получить ссылку. Страница чтения обслуживает только тех, у кого уже есть полный URL.

Почему превью в чате может сжечь единственное чтение

Типичная авария — не криптоаналитик, который сломал AES. Это робот превью, который нажал раньше коллеги. Вы вставляете полный URL в мессенджер. Чтобы нарисовать карточку, клиент сам запрашивает адрес. Документация Slack про разворот ссылок говорит прямо: по умолчанию, когда в сообщении появляется ссылка, Slack её запрашивает и предлагает превью. Похожее делают Teams, Discord, часть почтовых сканеров и превью в Telegram. Им нужны заголовок и краткое описание, не ваш пароль. Если «первый GET сразу забирает шифротекст и удаляет его», этот обход тратит единственное чтение. Коллега открывает уже уничтоженную страницу.

Ключ после # закрывает один слой от такого серверного запроса: робот просит s.html?id=…, фрагмент по спецификации в HTTP не уходит. Превью обычно не расшифрует, на карточке пароля нет. Сгореть может счётчик чтений, не сам ключ. Если страница чтения запрашивает шифротекст при загрузке, превью равносильно нажатию «Открыть и просмотреть» за получателя. Он видит 410. Вы думаете, что он уже прочитал.

Рабочий приём — не «никогда не вставляйте ссылку», а разделить «нарисовать страницу» и «забрать шифротекст». Сначала подтверждение и явное предупреждение, что нажатие потратит чтение; только потом запрос. Превью, которое тянет HTML и не нажимает кнопку, останется на шаге подтверждения. Это не остановит сканер, который выполняет скрипт и синтезирует клик, и не остановит человека, который ткнул не туда. Самый частый сбой «карточка открыла ссылку первой» перестаёт быть исходом по умолчанию.

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

Проверка на месте: остановитесь на подтверждении, затем ищите в Network

Слоган «уничтожается после прочтения» сам себя не доказывает. Видны четыре вещи: ушёл ли исходный текст при создании; запросила ли первая загрузка страницы чтения шифротекст; попал ли ключ в строку запроса после подтверждения; отвечает ли второе открытие 410, когда счётчик исчерпан.

Сначала канарейка. Откройте страницу создания одноразовой ссылки, введите одноразовую фразу, TTL — 1 час, число чтений оставьте 1. Никому не отправляйте. В блоке результата смотрите разделение: со стороны query только идентификатор, со стороны фрагмента — ключ. Затем DevTools → Network, включите Preserve log и поищите канарейку целиком. Тело запроса создания должно быть шифротекстом, не этой фразой. В аналитике её тоже быть не должно.

  1. Той же полной ссылкой откройте страницу чтения и пока не подтверждайте. В Network должны быть документ и статика, но не запрос шифротекста по id.
  2. Сравните адресную строку со строкой запроса документа. В адресе остаются # и то, что за ней. В строке запроса — только путь и ?id=.
  3. Нажмите «Открыть и просмотреть». Теперь должен появиться запрос шифротекста; после успеха страница покажет исходный текст. Откройте ту же ссылку второй раз: уничтожено или 410, а не та же канарейка.

Три чистых прохода поддерживают узкий вывод: в этом браузере, в этом прогоне исходный текст не ушёл как наблюдаемое рабочее поле, ключ не попал в строку запроса, шифротекст после одного успешного чтения больше нельзя забрать. Это не доказывает, что расширение не читало поле ввода, и не доказывает, что на скомпрометированном диске никогда не лежал старый шифротекст. Смените браузер — канарейку стоит прогнать снова. Если нужно ещё проверить, не покинул ли исходный текст любую страницу инструмента, шаги в заметке Шифрование в браузере: как убедиться, что исходный текст не покинул устройство.

Копию, снимок экрана и пересылку полной ссылки это не останавливает

Самоуничтожение снижает два риска: долгий исходный текст на сервере и повторные открытия одного и того же шифротекста. Третий риск оно не снижает: что получатель сделает, когда исходный текст уже на экране. Скопирует, снимет экран, перешлёт, прочитает вслух. Саму ссылку тоже можно переслать целиком — ключ и id поедут вместе — и следующий человек расшифрует на странице чтения, пока счётчик не исчерпан.

Поэтому «получатель должен войти в аккаунт» не закрывает передачу. Контроль доступа — это полный URL. Дверь с логином на странице чтения не прячет ключ от сервера: ключ туда и не должен уходить. Она только добавляет барьер получателю и меняет «у кого ссылка» на «у кого аккаунт». В MakePwd нет аккаунтов и нет хранилища паролей. Создать и прочитать можно без регистрации. Страница чтения открыта получателю.

Есть и предел объёма. 32 КБ хватает на пароль, API-ключ, код восстановления и короткую пометку. Не хватает на дамп базы или пачку сертификатов. Целый файл лучше собрать на этом устройстве в .lock / .enc и отправить через диск или почту. Шифрование файлов принимает один файл до 5 ГБ, тоже без аккаунта, исходный файл по умолчанию не загружается. Запихнуть большой файл в одноразовый текстовый канал — не «безопаснее». Это просто вне конструкции этой дорожки.

Отработайте состояние «уничтожено» на странице, которая открывается без аккаунта

Если нужен инструмент, который сразу показывает разделение query и фрагмента в результате, начните с одноразовой ссылки MakePwd. Открывается без регистрации с обеих сторон. При создании шифрование идёт в текущей вкладке, алгоритм — AES-256-GCM. Сервер получает шифротекст, ttl_hours (0–168, по умолчанию 24) и max_reads (1–10, по умолчанию 1). Вид ссылки всегда s.html?id={id}#{key}. Страница чтения сначала останавливается на подтверждении, затем забирает шифротекст и расшифровывает в этой вкладке.

Возьмите канарейку выше и оставьте число чтений равным 1. Смотрите сразу три места: разделение query / фрагмента при создании, есть ли в Network запрос шифротекста при первой загрузке страницы чтения, и уходит ли второе открытие в «уничтожено». Разделение подтверждает, что ключ не попал в query. Первая загрузка подтверждает, что превью не сожжёт ссылку само. Второе открытие подтверждает, что «после прочтения уничтожается» — не только фраза на странице.

Если перед отправкой ещё нужно снять метки слежения с обычной веб-ссылки, сначала пройдите удаление UTM, а короткий секрет положите в одноразовую ссылку. Очистка снимает UTM и идентификаторы клика из query. Она не заменяет канал с одноразовым шифротекстом. Ни один из этих шагов не требует входа, и ни один не показывает почту поддержки, которая не подключена.

Частые вопросы

После одного открытия на сервере ещё есть исходный текст?

Его там не было. Браузер сначала шифрует. Сервер получает только шифротекст. После успешного запроса и исчерпания счётчика шифротекст удаляют. Повторный запрос того же id должен увидеть 410 или «уже уничтожено». Строка URL может остаться в истории чата. Это не копия исходного текста на хосте.

Превью в чате сожжёт содержимое раньше получателя?

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

Чем 410 отличается от 404?

410 значит: ресурс был доступен и его убрали насовсем; клиенту не стоит повторять. 404 расплывчатее: неверный id или запись уже сняли. Одноразовой ссылке яснее, когда 410 отделяет уничтожение от истечения срока. В обоих случаях исходного текста для восстановления нет.

Нужно ли входить, чтобы открыть страницу чтения?

Нет. Создать и прочитать можно без аккаунта. Получатель забирает шифротекст по id и расшифровывает в этой вкладке ключом после решётки. В MakePwd нет аккаунтов и нет хранилища паролей. Контроль доступа — сама полная ссылка.

Три вещи, которые стоит помнить при следующей одноразовой передаче

Первое: спрашивая «что осталось после чтения», сначала спросите, лежал ли исходный текст на сервере. Не лежал. Удаляют шифротекст. URL в чате ничего не расшифрует. Второе: точка сгорания — запрос шифротекста, не первая отрисовка страницы чтения. Кнопка подтверждения для роботов превью, не для того, у кого уже вся строка. Третье: смотрите Network — ищите канарейку при создании, сверяйте строку запроса с # при чтении, на втором открытии ждите 410.

Если ещё нужно понять, почему ключ можно класть после решётки, читайте Почему ключ после решётки в URL удобно класть и когда эта защита ломается. Здесь вопрос «что остаётся на сервере после одного чтения» сведён к диапазону, который можно сверить с кодом ответа и моментом запроса.